From c6e77f6fa462e292db5f693a33e7c483b5a6e19e Mon Sep 17 00:00:00 2001
From: Kai Reinhard <K.Reinhard@micromata.de>
Date: Fri, 16 Apr 2021 23:59:02 +0000
Subject: [PATCH] Release 0.5 started: SpringBoot instead of Jetty, Jersey etc.

---
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/ServerConfiguration.kt                 |   50 +
 borgbutler-core/build.gradle                                                                            |   16 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/RequestLog.kt                     |  138 +++
 borgbutler-core/src/main/kotlin/de/micromata/borgbutler/config/Configuration.kt                         |    4 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/SystemInfo.kt                     |   33 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ConfigurationInfo.kt              |    9 
 borgbutler-webapp/src/components/views/archives/FileListPanel.jsx                                       |    2 
 borgbutler-core/src/main/kotlin/de/micromata/borgbutler/json/JsonUtils.kt                               |   69 +
 borgbutler-server/build.gradle                                                                          |   22 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LoggingEventData.java            |   26 
 build.gradle                                                                                            |   13 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/SystemInfoRest.kt                 |   26 
 doc/Development.adoc                                                                                    |    4 
 borgbutler-docker/app/Dockerfile                                                                        |    2 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserFilter.java                     |    5 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/WebConfig.kt                           |   30 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/I18nRest.kt                       |   42 +
 borgbutler-core/src/main/kotlin/de/micromata/borgbutler/BorgJob.kt                                      |    4 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/logging/LoggerMemoryAppender.kt        |  134 +++
 borgbutler-core/src/main/java/de/micromata/borgbutler/config/BorgRepoConfig.java                        |    6 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/SingleUserManager.java              |    6 
 borgbutler-webapp/src/components/views/archives/ArchiveView.jsx                                         |    2 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/FilesystemBrowserRest.kt          |  141 ++++
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ReposRest.kt                      |   66 +
 borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx                                    |    2 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ArchivesRest.kt                   |  221 ++++++
 borgbutler-server/src/main/resources/application.properties                                             |    2 
 /dev/null                                                                                               |   21 
 borgbutler-docker/app/entrypoint.sh                                                                     |    2 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/GlobalDefaultExceptionHandling.kt |   57 +
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/JobsRest.kt                       |  201 +++++
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/BorgButlerApplication.kt               |  179 +++++
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ConfigurationRest.kt              |   76 ++
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ExceptionStackTracePrinter.kt     |   72 ++
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/VersionRest.kt                    |   66 +
 borgbutler-core/src/main/kotlin/de/micromata/borgbutler/config/ConfigurationHandler.kt                  |    8 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/RestUtils.kt                      |  112 +++
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogLevel.java                    |    8 
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/LoggingRest.kt                    |   67 +
 borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/BorgRepoConfigsRest.kt            |   82 ++
 borgbutler-server/src/main/resources/logback-spring.xml                                                 |   35 +
 41 files changed, 1,974 insertions(+), 87 deletions(-)

diff --git a/borgbutler-core/build.gradle b/borgbutler-core/build.gradle
index 7a89458..f7a81d5 100644
--- a/borgbutler-core/build.gradle
+++ b/borgbutler-core/build.gradle
@@ -1,6 +1,5 @@
 plugins {
     id 'java'
-        id "org.jetbrains.kotlin.jvm" version "1.4.32"
 }
 
 description = 'borgbutler-core'
@@ -10,10 +9,10 @@
     implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.8.1'
     implementation group: 'org.apache.commons', name: 'commons-exec', version: '1.3'
     implementation group: 'org.apache.commons', name: 'commons-collections4', version: '4.2'
-    implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.18'
+    implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.20'
     implementation group: 'org.apache.commons', name: 'commons-jcs-core', version: '2.2.1'
     // https://mvnrepository.com/artifact/com.esotericsoftware/kryo
-    implementation group: 'com.esotericsoftware', name: 'kryo', version: '5.0.0-RC1'
+    implementation group: 'com.esotericsoftware', name: 'kryo', version: '5.1.0'
     // Serialization (faster than Java built-in)
     implementation 'io.github.microutils:kotlin-logging-jvm:2.0.6'
 
@@ -32,7 +31,6 @@
 }
 
 sourceSets {
-    main.kotlin.srcDirs += 'src/main/kotlin'
     main.java.srcDirs += 'src/main/java'
 }
 
@@ -41,13 +39,3 @@
     minHeapSize = "128m"
     maxHeapSize = "1500m"
 }
-compileKotlin {
-    kotlinOptions {
-        jvmTarget = "1.8"
-    }
-}
-compileTestKotlin {
-    kotlinOptions {
-        jvmTarget = "1.9"
-    }
-}
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/config/BorgRepoConfig.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/config/BorgRepoConfig.java
index a786023..ac82d24 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/config/BorgRepoConfig.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/config/BorgRepoConfig.java
@@ -1,6 +1,7 @@
 package de.micromata.borgbutler.config;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
+import de.micromata.borgbutler.json.JsonUtils;
 import org.apache.commons.lang3.StringUtils;
 
 import java.util.ArrayList;
@@ -93,4 +94,9 @@
     public void setId(String id) {
         this.id = id;
     }
+
+    @Override
+    public String toString() {
+        return JsonUtils.toJson(this);
+    }
 }
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/json/JsonUtils.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/json/JsonUtils.java
deleted file mode 100644
index 5263f72..0000000
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/json/JsonUtils.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package de.micromata.borgbutler.json;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.core.io.JsonStringEncoder;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.StringWriter;
-
-public class JsonUtils {
-    private static Logger log = LoggerFactory.getLogger(JsonUtils.class);
-
-    public static String toJson(Object obj) {
-        return toJson(obj, false);
-    }
-
-    /**
-     * @param obj
-     * @param prettyPrinter If true, the json output will be pretty printed (human readable with new lines and indenting).
-     * @return
-     */
-    public static String toJson(Object obj, boolean prettyPrinter) {
-        if (obj == null) {
-            return "";
-        }
-        ObjectMapper objectMapper = new ObjectMapper();
-        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
-        try {
-            if (prettyPrinter) {
-                return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
-            } else {
-                StringWriter writer = new StringWriter();
-                objectMapper.writeValue(writer, obj);
-                return writer.toString();
-            }
-        } catch (IOException ex) {
-            log.error(ex.getMessage(), ex);
-            return "";
-        }
-    }
-
-    public static String toJson(String str) {
-        if (str == null) return "";
-        return new String(JsonStringEncoder.getInstance().quoteAsString(str));
-    }
-
-    public static <T> T fromJson(Class<T> clazz, String json) {
-        ObjectMapper objectMapper = new ObjectMapper();
-        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-        try {
-            return objectMapper.readValue(json, clazz);
-        } catch (IOException ex) {
-            log.error(ex.getMessage(), ex);
-            return null;
-        }
-    }
-
-    public static <T> T fromJson(final TypeReference<T> type, final String json) {
-        try {
-            T data = new ObjectMapper().readValue(json, type);
-            return data;
-        } catch (Exception ex) {
-            log.error("Json: '" + json + "': " + ex.getMessage(), ex);
-        }
-        return null;
-    }
-}
diff --git a/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/BorgJob.kt b/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/BorgJob.kt
index 6f41b00..ca5d6c6 100644
--- a/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/BorgJob.kt
+++ b/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/BorgJob.kt
@@ -6,18 +6,20 @@
 import de.micromata.borgbutler.jobs.JobResult
 import de.micromata.borgbutler.json.JsonUtils
 import de.micromata.borgbutler.json.borg.ProgressInfo
+import mu.KotlinLogging
 import org.apache.commons.exec.CommandLine
 import org.apache.commons.exec.environment.EnvironmentUtils
 import org.apache.commons.lang3.StringUtils
 import org.slf4j.LoggerFactory
 import java.io.IOException
 
+private val log = KotlinLogging.logger {}
+
 /**
  * A queue is important because Borg doesn't support parallel calls for one repository.
  * For each repository one single queue is allocated.
  */
 open class BorgJob<T> : AbstractCommandLineJob, Cloneable {
-    private val log = LoggerFactory.getLogger(BorgJob::class.java)
     var command: BorgCommand? = null
         private set
 
diff --git a/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/config/Configuration.kt b/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/config/Configuration.kt
index 7eb3cbf..d8781f5 100644
--- a/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/config/Configuration.kt
+++ b/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/config/Configuration.kt
@@ -4,15 +4,17 @@
 import com.fasterxml.jackson.annotation.JsonProperty
 import de.micromata.borgbutler.config.BorgRepoConfig
 import de.micromata.borgbutler.demo.DemoRepos
+import mu.KotlinLogging
 import org.apache.commons.lang3.StringUtils
 import org.slf4j.LoggerFactory
 import java.io.File
 
+private val log = KotlinLogging.logger {}
+
 /**
  * Representation of ~/.borgbutler/borgbutler-config.json.
  */
 open class Configuration {
-    private val log = LoggerFactory.getLogger(Configuration::class.java)
 
     @JsonIgnore
     private var workingDir: File? = null
diff --git a/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/config/ConfigurationHandler.kt b/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/config/ConfigurationHandler.kt
index 2715560..d4f9444 100644
--- a/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/config/ConfigurationHandler.kt
+++ b/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/config/ConfigurationHandler.kt
@@ -101,14 +101,14 @@
         }
 
         @kotlin.jvm.JvmStatic
-        fun getInstance(): ConfigurationHandler? {
+        fun getInstance(): ConfigurationHandler {
             if (instance == null) instance = ConfigurationHandler()
-            return instance
+            return instance!!
         }
 
         @kotlin.jvm.JvmStatic
-        fun getConfiguration(): Configuration? {
-            return getInstance()!!.configuration
+        fun getConfiguration(): Configuration {
+            return getInstance().configuration!!
         }
 
         @kotlin.jvm.JvmStatic
diff --git a/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/json/JsonUtils.kt b/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/json/JsonUtils.kt
new file mode 100644
index 0000000..54864d7
--- /dev/null
+++ b/borgbutler-core/src/main/kotlin/de/micromata/borgbutler/json/JsonUtils.kt
@@ -0,0 +1,69 @@
+package de.micromata.borgbutler.json
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.ObjectMapper
+import mu.KotlinLogging
+import org.slf4j.LoggerFactory
+import java.io.IOException
+import java.io.StringWriter
+
+private val log = KotlinLogging.logger {}
+
+object JsonUtils {
+    /**
+     * @param obj
+     * @param prettyPrinter If true, the json output will be pretty printed (human readable with new lines and indenting).
+     * @return
+     */
+    @JvmOverloads
+    @JvmStatic
+    fun toJson(obj: Any?, prettyPrinter: Boolean? = false): String {
+        if (obj == null) {
+            return ""
+        }
+        val objectMapper = ObjectMapper()
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
+        return try {
+            if (prettyPrinter == true) {
+                objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj)
+            } else {
+                val writer = StringWriter()
+                objectMapper.writeValue(writer, obj)
+                writer.toString()
+            }
+        } catch (ex: IOException) {
+            log.error(ex.message, ex)
+            ""
+        }
+    }
+
+    @JvmStatic
+    fun toJson(str: String?): String {
+        return if (str == null) "" else String(JsonStringEncoder.getInstance().quoteAsString(str))
+    }
+
+    @JvmStatic
+    fun <T> fromJson(clazz: Class<T>?, json: String?): T? {
+        val objectMapper = ObjectMapper()
+        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+        return try {
+            objectMapper.readValue(json, clazz)
+        } catch (ex: IOException) {
+            log.error(ex.message, ex)
+            null
+        }
+    }
+
+    @JvmStatic
+    fun <T> fromJson(type: TypeReference<T>?, json: String): T? {
+        try {
+            return ObjectMapper().readValue(json, type)
+        } catch (ex: Exception) {
+            log.error("Json: '" + json + "': " + ex.message, ex)
+        }
+        return null
+    }
+}
diff --git a/borgbutler-docker/app/Dockerfile b/borgbutler-docker/app/Dockerfile
index 8ba9f29..a5b9096 100644
--- a/borgbutler-docker/app/Dockerfile
+++ b/borgbutler-docker/app/Dockerfile
@@ -16,7 +16,7 @@
 USER borgbutler:borgbutler
 
 # Don't put fat jar files in docker images: https://phauer.com/2019/no-fat-jar-in-docker-image/
-ARG DEPENDENCY=target/dependency/borgbutler-server-0.4-SNAPSHOT
+ARG DEPENDENCY=target/dependency/borgbutler-server-0.5-SNAPSHOT
 COPY ${DEPENDENCY}/lib /app/lib
 COPY ${DEPENDENCY}/web /app/web
 #COPY ${DEPENDENCY}/META-INF /app/META-INF
diff --git a/borgbutler-docker/app/entrypoint.sh b/borgbutler-docker/app/entrypoint.sh
index a969e6d..0d6e59a 100644
--- a/borgbutler-docker/app/entrypoint.sh
+++ b/borgbutler-docker/app/entrypoint.sh
@@ -49,7 +49,7 @@
 
 echo "Starting java ${JAVA_OPTS} -cp app/web/*:app/lib/* -DborgbutlerHome=/BorgButler/ -DapplicationHome=/app -DbindAddress=0.0.0.0 -DallowedClientIps=172.17. de.micromata.borgbutler.server.Main -q ${JAVA_ARGS}"
 
-java ${JAVA_OPTS} -cp app/web/*:app/lib/* -DborgbutlerHome=/BorgButler/ -DapplicationHome=/app -DbindAddress=0.0.0.0 -DallowedClientIps=172.17. de.micromata.borgbutler.server.Main -q ${JAVA_ARGS} &
+java $JAVA_OPTS -cp app/web/*:app/lib/* -DborgbutlerHome=/BorgButler/ -DapplicationHome=/app -DbindAddress=0.0.0.0 -DallowedClientIps=172.17. de.micromata.borgbutler.server.Main -q $JAVA_ARGS &
 
 CHILD=$!
 wait $CHILD
diff --git a/borgbutler-server/build.gradle b/borgbutler-server/build.gradle
index 62e4864..470ee6f 100644
--- a/borgbutler-server/build.gradle
+++ b/borgbutler-server/build.gradle
@@ -10,6 +10,7 @@
 
 plugins {
     id 'java'
+    id 'org.springframework.boot' version '2.4.5'
 }
 
 description = 'borgbutler-server'
@@ -21,20 +22,13 @@
     implementation group: 'org.apache.commons', name: 'commons-collections4', version: '4.2'
     implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.18'
     implementation group: 'commons-io', name: 'commons-io', version: '2.8.0'
-    implementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '9.4.12.v20180830'
-    implementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '9.4.12.v20180830'
-    implementation group: 'org.eclipse.jetty', name: 'jetty-servlets', version: '9.4.12.v20180830'
-    implementation group: 'org.glassfish.jaxb', name: 'jaxb-core', version: '2.3.0.1'
-    implementation group: 'org.glassfish.jaxb', name: 'jaxb-runtime', version: '2.3.1'
-    implementation group: 'org.glassfish.jersey.containers', name: 'jersey-container-servlet', version: '2.27'
-    implementation group: 'org.glassfish.jersey.media', name: 'jersey-media-multipart', version: '2.27'
-    implementation group: 'org.glassfish.jersey.media', name: 'jersey-media-json-jackson', version: '2.27'
-    implementation group: 'org.glassfish.jersey.inject', name: 'jersey-hk2', version: '2.27'
-    implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
-    implementation group: 'javax.xml.ws', name: 'jaxws-api', version: '2.3.1'
-    implementation group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25'
+
+    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web
+    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.4.5'
+    implementation 'io.github.microutils:kotlin-logging-jvm:2.0.6'
+
     // https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient
-    implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.6'
+    implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.13'
 
     // https://mvnrepository.com/artifact/commons-cli/commons-cli
     implementation group: 'commons-cli', name: 'commons-cli', version: '1.4'
@@ -49,7 +43,7 @@
 
 apply plugin: 'application'
 apply plugin: 'kotlin'
-mainClassName = "de.micromata.borgbutler.server.Main"
+mainClassName = "de.micromata.borgbutler.server.BorgButlerApplication"
 
 run() {
     doFirst {
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java
deleted file mode 100644
index 036d7fe..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java
+++ /dev/null
@@ -1,189 +0,0 @@
-package de.micromata.borgbutler.server;
-
-import de.micromata.borgbutler.cache.ButlerCache;
-import de.micromata.borgbutler.config.ConfigurationHandler;
-import de.micromata.borgbutler.json.borg.BorgFilesystemItem;
-import de.micromata.borgbutler.server.jetty.JettyServer;
-import de.micromata.borgbutler.server.user.SingleUserManager;
-import de.micromata.borgbutler.server.user.UserManager;
-import org.apache.commons.cli.*;
-import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.awt.*;
-import java.io.*;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.List;
-import java.util.TimeZone;
-
-public class Main {
-    private static Logger log = LoggerFactory.getLogger(Main.class);
-
-    private static final Main main = new Main();
-
-    private JettyServer server;
-    private boolean shutdownInProgress;
-
-    private Main() {
-    }
-
-    public static void main(String[] args) {
-        main._start(args);
-    }
-
-    public static JettyServer startUp(String... restPackageNames) {
-        return main._startUp(restPackageNames);
-    }
-
-    public static void shutdown() {
-        main._shutdown();
-    }
-
-
-    private void _start(String[] args) {
-        ConfigurationHandler.setConfigClazz(ServerConfiguration.class);
-        // create Options object
-        Options options = new Options();
-        options.addOption("e", "extract-archive-content", true, "Extracts the content of an archive cache file only (doesn't start the server). A complete file list of the archive will be extracted to stdout.");
-        options.addOption("p", "port", true, "The default port for the web server.");
-        options.addOption("q", "quiet", false, "Don't open browser automatically.");
-        options.addOption("h", "help", false, "Print this help screen.");
-        //options.addOption("homeDir", true, "Specify own home directory of butler. Default is $HOME/.borgbutler");
-        CommandLineParser parser = new DefaultParser();
-        try {
-            // parse the command line arguments
-            CommandLine line = parser.parse(options, args);
-            if (line.hasOption('h')) {
-                printHelp(options);
-                return;
-            }
-            if (line.hasOption('e')) {
-                String file = line.getOptionValue("e");
-                printArchiveContent(file);
-                return;
-            }
-            if (line.hasOption('p')) {
-                // initialise the member variable
-                String portString = line.getOptionValue("p");
-                try {
-                    int port = Integer.parseInt(portString);
-                    if (port < 1 || port > 65535) {
-                        System.err.println("Port outside range.");
-                        return;
-                    }
-                    ServerConfiguration.get().setPort(port);
-                } catch (NumberFormatException ex) {
-                    printHelp(options);
-                    return;
-                }
-            }
-            String applicationHome = System.getProperty("borgbutlerHome");
-            if (applicationHome != null) {
-                ConfigurationHandler.init(applicationHome);
-            }
-            if (Desktop.isDesktopSupported()) {
-                RunningMode.setServerType(RunningMode.ServerType.DESKTOP);
-            } else {
-                RunningMode.setServerType(RunningMode.ServerType.SERVER);
-            }
-            RunningMode.logMode();
-            Runtime.getRuntime().addShutdownHook(new Thread() {
-                @Override
-                public void run() {
-                    main._shutdown();
-                }
-            });
-
-            JettyServer server = startUp();
-            BorgInstallation.getInstance().initialize();
-            if (!line.hasOption('q')) {
-
-                try {
-                    java.awt.Desktop.getDesktop().browse(java.net.URI.create(server.getUrl()));
-                } catch (Exception ex) {
-                    log.info("Can't open web browser: " + ex.getMessage());
-                }
-            } else {
-                log.info("Please open your browser: " + server.getUrl().replace("0.0.0.0", "127.0.0.1")); // 0.0.0.0 for Docker installations.
-            }
-        } catch (ParseException ex) {
-            // oops, something went wrong
-            System.err.println("Parsing failed.  Reason: " + ex.getMessage());
-            printHelp(options);
-        }
-    }
-
-    private JettyServer _startUp(String... restPackageNames) {
-        server = new JettyServer();
-        server.start(restPackageNames);
-
-        UserManager.setUserManager(new SingleUserManager());
-
-        return server;
-    }
-
-    private void _shutdown() {
-        if (server == null) {
-            // Do nothing (server wasn't started).
-            return;
-        }
-        synchronized (this) {
-            if (shutdownInProgress == true) {
-                // Another thread already called this method. There is nothing further to do.
-                return;
-            }
-            shutdownInProgress = true;
-        }
-        log.info("Shutting down BorgButler web server...");
-        server.stop();
-        ButlerCache.getInstance().shutdown();
-    }
-
-    private static void printHelp(Options options) {
-        HelpFormatter formatter = new HelpFormatter();
-        formatter.printHelp("borgbutler-server", options);
-    }
-
-    private static void printArchiveContent(String fileName) {
-        File file = new File(fileName);
-        List<BorgFilesystemItem> fileList = ButlerCache.getInstance().getArchiveContent(file);
-        boolean parseFormatExceptionPrinted = false;
-        if (fileList != null && fileList.size() > 0) {
-            TimeZone tz = TimeZone.getTimeZone("UTC");
-            DateFormat iso = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss'Z'"); // Quoted "Z" to indicate UTC, no timezone offset
-            iso.setTimeZone(tz);
-            DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S");
-            File out = new File(FilenameUtils.getBaseName(fileName) + ".txt.gz");
-            log.info("Writing file list to: " + out.getAbsolutePath());
-            try (PrintWriter writer = new PrintWriter(new BufferedOutputStream(new GzipCompressorOutputStream(new FileOutputStream(out))))) {
-                for (BorgFilesystemItem item : fileList) {
-                    String time = item.getMtime();
-                    if (time.indexOf('T') > 0) {
-                        try {
-                            Date date = df.parse(item.getMtime());
-                            time = iso.format(date);
-                        } catch (java.text.ParseException ex) {
-                            if (!parseFormatExceptionPrinted) {
-                                parseFormatExceptionPrinted = true;
-                                log.error("Can't parse date: " + item.getMtime());
-                            }
-                        }
-                    }
-                    writer.write(item.getMode() + " " + item.getUser() + " "
-                            + StringUtils.rightPad(FileUtils.byteCountToDisplaySize(item.getSize()), 10)
-                            + " " + time + " " + item.getPath());
-                    writer.write("\n");
-                }
-            } catch (IOException ex) {
-                log.error("Can't write file '" + out.getAbsolutePath() + "': " + ex.getMessage());
-            }
-        }
-        // 2018-12-04T22:44:58.924642
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java
deleted file mode 100644
index 8e24c4e..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package de.micromata.borgbutler.server;
-
-import org.apache.commons.lang3.StringUtils;
-import de.micromata.borgbutler.config.Configuration;
-import de.micromata.borgbutler.config.ConfigurationHandler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class ServerConfiguration extends Configuration {
-    private static Logger log = LoggerFactory.getLogger(ServerConfiguration.class);
-    private final static String[] SUPPORTED_LANGUAGES = {"en", "de"};
-    public static final int WEBSERVER_PORT_DEFAULT = 9042;
-    private static final boolean WEB_DEVELOPMENT_MODE_PREF_DEFAULT = false;
-
-    private static String applicationHome;
-
-    private int port = WEBSERVER_PORT_DEFAULT;
-    private boolean webDevelopmentMode = WEB_DEVELOPMENT_MODE_PREF_DEFAULT;
-
-    public static ServerConfiguration get() {
-        return (ServerConfiguration)ConfigurationHandler.getConfiguration();
-    }
-
-    public static String[] getSupportedLanguages() {
-        return SUPPORTED_LANGUAGES;
-    }
-
-    public static String getApplicationHome() {
-        if (applicationHome == null) {
-            applicationHome = System.getProperty("applicationHome");
-            if (StringUtils.isBlank(applicationHome)) {
-                applicationHome = System.getProperty("user.dir");
-                log.info("applicationHome is not given as JVM   parameter. Using current working dir (OK for start in IDE): " + applicationHome);
-            }
-        }
-        return applicationHome;
-    }
-
-    public int getPort() {
-        return port;
-    }
-
-    public void setPort(int port) {
-        this.port = port;
-    }
-
-    /**
-     * If true, CrossOriginFilter will be set.
-     */
-    public boolean isWebDevelopmentMode() {
-        return webDevelopmentMode;
-    }
-
-    public void setWebDevelopmentMode(boolean webDevelopmentMode) {
-        this.webDevelopmentMode = webDevelopmentMode;
-    }
-
-    public void copyFrom(ServerConfiguration other) {
-        super.copyFrom(other);
-        this.port = other.port;
-        this.webDevelopmentMode = other.webDevelopmentMode;
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/jetty/JettyServer.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/jetty/JettyServer.java
deleted file mode 100644
index 5f9c29c..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/jetty/JettyServer.java
+++ /dev/null
@@ -1,188 +0,0 @@
-package de.micromata.borgbutler.server.jetty;
-
-import de.micromata.borgbutler.server.ServerConfiguration;
-import de.micromata.borgbutler.server.RunningMode;
-import de.micromata.borgbutler.server.rest.ConfigurationRest;
-import de.micromata.borgbutler.server.user.UserFilter;
-import org.apache.commons.lang3.ArrayUtils;
-import org.eclipse.jetty.server.Connector;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.servlet.*;
-import org.eclipse.jetty.servlets.CrossOriginFilter;
-import org.eclipse.jetty.util.resource.Resource;
-import org.glassfish.jersey.jackson.JacksonFeature;
-import org.glassfish.jersey.media.multipart.MultiPartFeature;
-import org.glassfish.jersey.server.ResourceConfig;
-import org.glassfish.jersey.servlet.ServletContainer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.servlet.DispatcherType;
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.ServerSocket;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.EnumSet;
-
-public class JettyServer {
-    private static Logger log = LoggerFactory.getLogger(JettyServer.class);
-    private static String BIND_ADDRESS = "127.0.0.1";
-    private static final int MAX_PORT_NUMBER = 65535;
-    private Server server;
-    private int port;
-
-    static {
-        String bindAddress = System.getProperty("bindAddress");
-        if (bindAddress != null) {
-            BIND_ADDRESS = bindAddress;
-        }
-        log.info("Binding server to address: " + BIND_ADDRESS);
-    }
-
-    public static String getBindAddress() {
-        return BIND_ADDRESS;
-    }
-
-    public void start(String... restPackageNames) {
-        port = findFreePort();
-        if (port == -1) {
-            return;
-        }
-        log.info("Starting web server on port " + port);
-        server = new Server();
-
-        ServerConnector connector = new ServerConnector(server);
-        connector.setHost(BIND_ADDRESS);
-        connector.setPort(port);
-        server.setConnectors(new Connector[]{connector});
-
-        ServletContextHandler ctx =
-                new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
-        ctx.setContextPath("/");
-
-        ResourceConfig resourceConfig = new ResourceConfig();
-        String[] packageNames = {ConfigurationRest.class.getPackage().getName()};
-        if (restPackageNames != null && restPackageNames.length > 0) {
-            packageNames = (String[]) ArrayUtils.addAll(packageNames, restPackageNames);
-        }
-        resourceConfig.packages(packageNames);
-        resourceConfig.register(MultiPartFeature.class)
-                .register(JacksonFeature.class);
-        //   .register(LoggingFilter.class)
-        //   .property("jersey.config.server.tracing.type", "ALL")
-        //   .property("jersey.config.server.tracing.threshold", "VERBOSE"))
-        ServletHolder jerseyServlet = new ServletHolder(
-                new ServletContainer(resourceConfig));
-        jerseyServlet.setInitOrder(1);
-        ctx.addServlet(jerseyServlet, "/rest/*");
-        ctx.addFilter(UserFilter.class, "/rest/*", EnumSet.of(DispatcherType.INCLUDE, DispatcherType.REQUEST));
-        // Following code doesn't work:
-        // jerseyServlet.setInitParameter("useFileMappedBuffer", "false");
-        // jerseyServlet.setInitParameter("cacheControl","max-age=0,public");
-
-        try {
-            Path path;
-            if (RunningMode.isDevelopmentMode()) {
-                path = Paths.get(ServerConfiguration.getApplicationHome(), "borgbutler-webapp", "build");
-            } else {
-                path = Paths.get(ServerConfiguration.getApplicationHome(), "web");
-            }
-            if (!Files.exists(path)) {
-                log.error("********** Fatal: Can't find web archive: " + path.toAbsolutePath());
-            }
-            URL url = path.toUri().toURL();
-            log.debug("Using web directory: " + url);
-            ctx.setBaseResource(Resource.newResource(url));
-        } catch (IOException ex) {
-            log.error(ex.getMessage(), ex);
-            return;
-        }
-        ctx.setWelcomeFiles(new String[]{"index.html"});
-        ctx.setInitParameter(DefaultServlet.CONTEXT_INIT + "cacheControl", "no-store,no-cache,must-revalidate");//"max-age=5,public");
-        ctx.setInitParameter(DefaultServlet.CONTEXT_INIT + "useFileMappedBuffer", "false");
-        ctx.addServlet(DefaultServlet.class, "/");
-
-        ErrorPageErrorHandler errorHandler = new ErrorPageErrorHandler();
-        errorHandler.addErrorPage(404, "/");
-        ctx.setErrorHandler(errorHandler);
-
-        if (RunningMode.isDevelopmentMode() || ServerConfiguration.get().isWebDevelopmentMode()) {
-            log.warn("*********************************");
-            log.warn("***********            **********");
-            log.warn("*********** ATTENTION! **********");
-            log.warn("***********            **********");
-            log.warn("*********** Running in **********");
-            log.warn("*********** dev mode!  **********");
-            log.warn("***********            **********");
-            log.warn("*********************************");
-            log.warn("Don't deliver this app in dev mode due to security reasons (CrossOriginFilter is set)!");
-
-            FilterHolder filterHolder = ctx.addFilter(CrossOriginFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
-            filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
-            filterHolder.setInitParameter(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*");
-            filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,POST,HEAD");
-            filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "X-Requested-With,Content-Type,Accept,Origin");
-        }
-
-        server.setHandler(ctx);
-
-        try {
-            server.start();
-        } catch (Exception ex) {
-            log.error("Can't start jetty: " + ex.getMessage(), ex);
-        }
-    }
-
-    public void stop() {
-        log.info("Stopping web server.");
-        try {
-            server.stop();
-        } catch (Exception ex) {
-            log.error("Can't stop web server: " + ex.getMessage(), ex);
-        }
-        if (server != null) {
-            server.destroy();
-        }
-    }
-
-    private int findFreePort() {
-        int port = ServerConfiguration.get().getPort();
-        return findFreePort(port);
-    }
-
-    private int findFreePort(int startPort) {
-        int port = startPort > 0 ? startPort : 1;
-        if (port > MAX_PORT_NUMBER) {
-            log.warn("Port can't be higher than " + MAX_PORT_NUMBER + ": " + port + ". It's a possible mis-configuration.");
-            port = ServerConfiguration.WEBSERVER_PORT_DEFAULT;
-        }
-        for (int i = port; i < port + 10; i++) {
-            try (ServerSocket socket = new ServerSocket()) {
-                socket.bind(new InetSocketAddress(BIND_ADDRESS, i));
-                return i;
-            } catch (Exception ex) {
-                log.info("Port " + i + " already in use or not available. Trying next port.");
-                continue; // try next port
-            }
-        }
-        if (startPort != ServerConfiguration.WEBSERVER_PORT_DEFAULT) {
-            log.info("Trying to fix port due to a possible mis-configuration.");
-            return findFreePort(ServerConfiguration.WEBSERVER_PORT_DEFAULT);
-        }
-        log.error("No free port found! Giving up.");
-        return -1;
-    }
-
-    public int getPort() {
-        return port;
-    }
-
-    public String getUrl() {
-        return "http://" + BIND_ADDRESS + ":" + port + "/";
-    }
-}
-
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/Log4jMemoryAppender.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/Log4jMemoryAppender.java
deleted file mode 100644
index 5218556..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/Log4jMemoryAppender.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package de.micromata.borgbutler.server.logging;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.log4j.AppenderSkeleton;
-import org.apache.log4j.spi.LoggingEvent;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-
-public class Log4jMemoryAppender extends AppenderSkeleton {
-    private static final int MAX_RESULT_SIZE = 1000;
-    private static final int QUEUE_SIZE = 10000;
-    private static Log4jMemoryAppender instance;
-
-    private int lastLogEntryOrderNumber = -1;
-
-    public static Log4jMemoryAppender getInstance() {
-        return instance;
-    }
-
-    public Log4jMemoryAppender() {
-        if (instance != null) {
-            throw new IllegalArgumentException("Log4jMemoryAppender shouldn't be instantiated twice!");
-        }
-        instance = this;
-    }
-
-    /**
-     * For test purposes.
-     */
-    Log4jMemoryAppender(boolean ignoreMultipleInstance) {
-
-    }
-
-    FiFoBuffer<LoggingEventData> queue = new FiFoBuffer<>(QUEUE_SIZE);
-
-    @Override
-    protected void append(LoggingEvent event) {
-        LoggingEventData eventData = new LoggingEventData(event);
-        eventData.orderNumber = ++lastLogEntryOrderNumber;
-        queue.add(eventData);
-    }
-
-    /**
-     * For testing purposes.
-     *
-     * @param event
-     */
-    void append(LoggingEventData event) {
-        queue.add(event);
-    }
-
-    public List<LoggingEventData> query(LogFilter filter, Locale locale) {
-        List<LoggingEventData> result = new ArrayList<>();
-        if (filter == null) {
-            return result;
-        }
-        int maxSize = filter.getMaxSize() != null ? filter.getMaxSize() : MAX_RESULT_SIZE;
-        if (maxSize > MAX_RESULT_SIZE) {
-            maxSize = MAX_RESULT_SIZE;
-        }
-        int counter = 0;
-        //I18n i18n = CoreI18n.getDefault().get(locale);
-        if (filter.isAscendingOrder()) {
-            for (int i = 0; i < queue.getSize(); i++) {
-                LoggingEventData resultEvent = getResultEvent(filter, queue.get(i), locale);
-                if (resultEvent == null) continue;
-                result.add(resultEvent);
-                if (++counter > maxSize) break;
-            }
-        } else {
-            for (int i = queue.getSize(); i >= 0; i--) {
-                LoggingEventData resultEvent = getResultEvent(filter, queue.get(i), locale);
-                if (resultEvent == null) continue;
-                result.add(resultEvent);
-                if (++counter > maxSize) break;
-            }
-        }
-        return result;
-    }
-
-    private LoggingEventData getResultEvent(LogFilter filter, LoggingEventData event, Locale locale) {
-        if (event == null) {
-            return null;
-        }
-        if (!event.getLevel().matches(filter.getThreshold())) {
-            return null;
-        }
-        if (filter.getLastReceivedLogOrderNumber() != null) {
-            if (event.getOrderNumber() <= filter.getLastReceivedLogOrderNumber()) {
-                return null;
-            }
-        }
-        String logString = null;
-        String message = event.getMessage();
-        boolean localizedMessage = false;
-            /*if (message != null && message.startsWith("i18n=")) {
-                I18nLogEntry i18nLogEntry = I18nLogEntry.parse(message);
-                message = i18n.formatMessage(i18nLogEntry.getI18nKey(), (Object[])i18nLogEntry.getArgs());
-                localizedMessage = true;
-            }*/
-
-        if (StringUtils.isNotBlank(filter.getSearch())) {
-            StringBuilder sb = new StringBuilder();
-            sb.append(event.getLogDate());
-            append(sb, event.getLevel(), true);
-            append(sb, message, true);
-            append(sb, event.getJavaClass(), true);
-            append(sb, event.getStackTrace(), filter.isShowStackTraces());
-            logString = sb.toString();
-        }
-        if (logString == null || matches(logString, filter.getSearch())) {
-            LoggingEventData resultEvent = event;
-            if (localizedMessage) {
-                // Need a clone
-                resultEvent = event.clone();
-                resultEvent.setMessage(message);
-            }
-            return resultEvent;
-        }
-        return null;
-    }
-
-    private void append(StringBuilder sb, Object value, boolean append) {
-        if (!append || value == null) {
-            return;
-        }
-        sb.append("|#|").append(value);
-    }
-
-    public void close() {
-    }
-
-    public boolean requiresLayout() {
-        return false;
-    }
-
-    private boolean matches(String str, String searchString) {
-        if (StringUtils.isBlank(str)) {
-            return StringUtils.isBlank(searchString);
-        }
-        if (StringUtils.isBlank(searchString)) {
-            return true;
-        }
-        return str.toLowerCase().contains(searchString.toLowerCase());
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogLevel.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogLevel.java
index a4f8b69..41ecebb 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogLevel.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogLevel.java
@@ -1,8 +1,8 @@
 package de.micromata.borgbutler.server.logging;
 
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.log4j.Level;
-import org.apache.log4j.spi.LoggingEvent;
 
 public enum LogLevel {
     ERROR, WARN, INFO, DEBUG, TRACE;
@@ -18,10 +18,8 @@
         return this.ordinal() <= treshold.ordinal();
     }
 
-    public static LogLevel getLevel(LoggingEvent event) {
+    public static LogLevel getLevel(ILoggingEvent event) {
         switch (event.getLevel().toInt()) {
-            case Level.ERROR_INT:
-                return LogLevel.ERROR;
             case Level.INFO_INT:
                 return LogLevel.INFO;
             case Level.DEBUG_INT:
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LoggingEventData.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LoggingEventData.java
index 2c39631..f00c60c 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LoggingEventData.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LoggingEventData.java
@@ -1,8 +1,10 @@
 package de.micromata.borgbutler.server.logging;
 
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+import ch.qos.logback.classic.spi.ThrowableProxyUtil;
+import ch.qos.logback.core.CoreConstants;
 import org.apache.commons.lang3.ClassUtils;
-import org.apache.log4j.spi.LocationInfo;
-import org.apache.log4j.spi.LoggingEvent;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -23,26 +25,26 @@
     private String logDate;
     String javaClass;
     private String javaClassSimpleName;
-    private String lineNumber;
+    private int lineNumber;
     private String methodName;
     private String stackTrace;
 
     LoggingEventData() {
-
     }
 
-    public LoggingEventData(LoggingEvent event) {
+    public LoggingEventData(ILoggingEvent event) {
         level = LogLevel.getLevel(event);
-        message = event.getRenderedMessage();
+        message = event.getFormattedMessage();
         messageObjectClass = event.getMessage().getClass().toString();
         loggerName = event.getLoggerName();
-        logDate = getIsoLogDate(event.timeStamp);
-        LocationInfo info = event.getLocationInformation();
-        Throwable throwable = event.getThrowableInformation() != null ? event.getThrowableInformation().getThrowable() : null;
-        if (throwable != null) {
+        logDate = getIsoLogDate(event.getTimeStamp());
+        StackTraceElement info = event.getCallerData()[0];
+        IThrowableProxy throwableProxy = event.getThrowableProxy();
+        if (throwableProxy != null) {
             StringWriter writer = new StringWriter();
             PrintWriter printWriter = new PrintWriter(writer);
-            throwable.printStackTrace(printWriter);
+            printWriter.append(ThrowableProxyUtil.asString(throwableProxy));
+            printWriter.append(CoreConstants.LINE_SEPARATOR);
             stackTrace = writer.toString();
         }
         if (info != null) {
@@ -81,7 +83,7 @@
         return javaClassSimpleName;
     }
 
-    public String getLineNumber() {
+    public int getLineNumber() {
         return lineNumber;
     }
 
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ArchivesRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ArchivesRest.java
deleted file mode 100644
index 90fe225..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ArchivesRest.java
+++ /dev/null
@@ -1,220 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.BorgCommands;
-import de.micromata.borgbutler.cache.ButlerCache;
-import de.micromata.borgbutler.config.BorgRepoConfig;
-import de.micromata.borgbutler.config.ConfigurationHandler;
-import de.micromata.borgbutler.data.Archive;
-import de.micromata.borgbutler.data.DiffFileSystemFilter;
-import de.micromata.borgbutler.data.FileSystemFilter;
-import de.micromata.borgbutler.data.Repository;
-import de.micromata.borgbutler.json.JsonUtils;
-import de.micromata.borgbutler.json.borg.BorgFilesystemItem;
-import de.micromata.borgbutler.utils.DirUtils;
-import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.math.NumberUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import java.awt.*;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.util.List;
-
-@Path("/archives")
-public class ArchivesRest {
-    private static Logger log = LoggerFactory.getLogger(ArchivesRest.class);
-
-    /**
-     * @param repoName      Name of repository ({@link Repository#getName()}.
-     * @param archiveId     Id or name of archive.
-     * @param prettyPrinter If true then the json output will be in pretty format.
-     * @return Repository (including list of archives) as json string.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    public String getArchive(@QueryParam("repo") String repoName,
-                             @QueryParam("archiveId") String archiveId, @QueryParam("force") boolean force,
-                             @QueryParam("prettyPrinter") boolean prettyPrinter) {
-        Archive archive = ButlerCache.getInstance().getArchive(repoName, archiveId, force);
-        if (force) {
-            ButlerCache.getInstance().deleteCachedArchiveContent(repoName, archiveId);
-        }
-        return JsonUtils.toJson(archive, prettyPrinter);
-    }
-
-    /**
-     * @param archiveId                     Id or name of archive.
-     * @param searchString                  The string to search for (key words separated by white chars, trailing ! char represents exclude).
-     * @param mode                          Flat (default) or tree.
-     * @param currentDirectory              The current displayed directory (only files and directories contained will be returned).
-     * @param maxResultSize                 maximum number of file items to return (default is 50).
-     * @param diffArchiveId                 If given, the differences between archiveId and diffArchiveId will be returned.
-     * @param autoChangeDirectoryToLeafItem If given, this method will step automatically into single sub directories.
-     * @param force                         If false (default), non cached file lists will not be loaded by borg.
-     * @param prettyPrinter                 If true then the json output will be in pretty format.
-     * @return Repository (including list of archives) as json string.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Path("filelist")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String getArchiveFileList(@QueryParam("archiveId") String archiveId,
-                                     @QueryParam("searchString") String searchString,
-                                     @QueryParam("mode") String mode,
-                                     @QueryParam("currentDirectory") String currentDirectory,
-                                     @QueryParam("maxResultSize") String maxResultSize,
-                                     @QueryParam("diffArchiveId") String diffArchiveId,
-                                     @QueryParam("autoChangeDirectoryToLeafItem") boolean autoChangeDirectoryToLeafItem,
-                                     @QueryParam("force") boolean force,
-                                     @QueryParam("prettyPrinter") boolean prettyPrinter) {
-        boolean diffMode = StringUtils.isNotBlank(diffArchiveId);
-        int maxSize = NumberUtils.toInt(maxResultSize, 50);
-        FileSystemFilter filter = diffMode ? new DiffFileSystemFilter() : new FileSystemFilter();
-        filter.setSearchString(searchString)
-                .setCurrentDirectory(currentDirectory)
-                .setAutoChangeDirectoryToLeafItem(autoChangeDirectoryToLeafItem);
-        List<BorgFilesystemItem> items = null;
-        if (diffMode) {
-            filter.setMode(FileSystemFilter.Mode.FLAT);
-            items = ButlerCache.getInstance().getArchiveContent(archiveId, true, filter);
-            List<BorgFilesystemItem> diffItems = ButlerCache.getInstance().getArchiveContent(diffArchiveId, true,
-                    filter);
-            filter.setMaxResultSize(maxSize)
-                    .setMode(mode);
-            items = ((DiffFileSystemFilter) filter).extractDifferences(items, diffItems);
-            items = filter.reduce(items);
-        } else {
-            filter.setMode(mode)
-                    .setMaxResultSize(maxSize);
-            // Get file list (without running diff).
-            items = ButlerCache.getInstance().getArchiveContent(archiveId, force,
-                    filter);
-            if (items == null) {
-                return "[{\"mode\": \"notLoaded\"}]";
-            }
-        }
-        return JsonUtils.toJson(items, prettyPrinter);
-    }
-
-    /**
-     * @param archiveId
-     * @param openDownloads
-     * @param fileNumber    The fileNumber of the file or directory in the archive served by BorgButler's
-     */
-    @GET
-    @Path("/restore")
-    @Produces(MediaType.APPLICATION_OCTET_STREAM)
-    public Response restore(@QueryParam("archiveId") String archiveId,
-                            @QueryParam("openDownloads") boolean openDownloads,
-                            @QueryParam("fileNumber") int fileNumber) {
-        log.info("Requesting file #" + fileNumber + " of archive '" + archiveId + "'.");
-        FileSystemFilter filter = new FileSystemFilter().setFileNumber(fileNumber);
-        List<BorgFilesystemItem> items = ButlerCache.getInstance().getArchiveContent(archiveId, false,
-                filter);
-        if (CollectionUtils.isEmpty(items)) {
-            log.error("Requested file #" + fileNumber + " not found in archive '" + archiveId
-                    + ". (May-be the archive content isn't yet loaded to the cache.");
-            Response.ResponseBuilder builder = Response.status(Response.Status.NOT_FOUND);
-            return builder.build();
-        }
-        if (items.size() != 1) {
-            log.error("Requested file #" + fileNumber + " found multiple times (" + items.size() + ") in archive '" + archiveId
-                    + "! Please remove the archive files (may-be corrupted).");
-            Response.ResponseBuilder builder = Response.status(404);
-            return builder.build();
-        }
-        Archive archive = ButlerCache.getInstance().getArchive(archiveId);
-        if (archive == null) {
-            Response.ResponseBuilder builder = Response.status(Response.Status.NOT_FOUND);
-            return builder.build();
-        }
-        BorgRepoConfig repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(archive.getRepoId());
-        try {
-            BorgFilesystemItem item = items.get(0);
-            File restoreHomeDir = ConfigurationHandler.getConfiguration().getRestoreHomeDir();
-            File restoreDir = BorgCommands.extractFiles(restoreHomeDir, repoConfig, archive, item.getPath());
-            List<java.nio.file.Path> files = DirUtils.listFiles(restoreDir.toPath());
-            if (CollectionUtils.isEmpty(files)) {
-                log.error("No files extracted.");
-                Response.ResponseBuilder builder = Response.status(Response.Status.NOT_FOUND);
-                return builder.build();
-            }
-            if (openDownloads)
-                openFileBrowser(new File(restoreDir, item.getPath()));
-            Response.ResponseBuilder builder = Response.status(Response.Status.ACCEPTED);
-            return builder.build();
-        } catch (IOException ex) {
-            log.error("No file extracted: " + ex.getMessage(), ex);
-            Response.ResponseBuilder builder = Response.status(Response.Status.NOT_FOUND);
-            return builder.build();
-        }
-    }
-
-    private void openFileBrowser(File fileDirectory) {
-        if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) {
-            File file = fileDirectory;
-            if (!fileDirectory.exists() || Files.isSymbolicLink(fileDirectory.toPath())) {
-                // Open parent.
-                file = fileDirectory.getParentFile();
-            }
-            Desktop.getDesktop().browseFileDirectory(file);
-        }
-    }
-
-    private Response handleRestoredFiles(BorgRepoConfig repoConfig, Archive archive) {
-        // Todo: Handle download of single files as well as download of zip archive (if BorgButler runs remote).
-        return null;
-       /* File file = path.toFile();
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        try {
-            FileUtils.copyFile(file, baos);
-        } catch (IOException ex) {
-            log.error(ex.getMessage(), ex);
-        }
-        BorgFilesystemItem item = items.get(0);
-        file = new File(item.getPath());
-        byte[] byteArray = baos.toByteArray();//result.getAsByteArrayOutputStream().toByteArray();
-        Response.ResponseBuilder builder = Response.ok(byteArray);
-        builder.header("Content-Disposition", "attachment; filename=" + file.getName());
-        // Needed to get the Content-Disposition by client:
-        builder.header("Access-Control-Expose-Headers", "Content-Disposition");
-        Response response = builder.build();
-        return response;
-
-        try {
-            //java.nio.file.Path tempDirWithPrefix = Files.createTempDirectory("borgbutler-extract-");
-            File restoreHomeDir = ConfigurationHandler.getConfiguration().getRestoreHomeDir();
-            File restoreDir = BorgCommands.extractFiles(restoreHomeDir, repoConfig, archive.getName(), item.getPath());
-            openFileBrowser(restoreDir);
-            List<java.nio.file.Path> files = DirUtils.listFiles(tempDir);
-            if (CollectionUtils.isEmpty(files)) {
-                log.error("No file extracted.");
-                Response.ResponseBuilder builder = Response.status(404);
-                return builder.build();
-            }
-            path = files.get(0);
-        } catch (IOException ex) {
-            log.error("No file extracted: " + ex.getMessage(), ex);
-            Response.ResponseBuilder builder = Response.status(404);
-            return builder.build();
-        } finally {
-           if (tempDir != null) {
-                try {
-                    FileUtils.deleteDirectory(tempDir.toFile());
-                } catch (IOException ex) {
-                    log.error("Error while trying to delete temporary directory '" + tempDir.toString() + "': " + ex.getMessage(), ex);
-                }
-            }
-        }*/
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/BorgRepoConfigsRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/BorgRepoConfigsRest.java
deleted file mode 100644
index 0ae66d6..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/BorgRepoConfigsRest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.BorgCommandResult;
-import de.micromata.borgbutler.BorgCommands;
-import de.micromata.borgbutler.cache.ButlerCache;
-import de.micromata.borgbutler.config.BorgRepoConfig;
-import de.micromata.borgbutler.config.ConfigurationHandler;
-import de.micromata.borgbutler.data.Repository;
-import de.micromata.borgbutler.jobs.JobResult;
-import de.micromata.borgbutler.json.JsonUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.ws.rs.*;
-import javax.ws.rs.core.MediaType;
-
-@Path("/repoConfig")
-public class BorgRepoConfigsRest {
-    private static Logger log = LoggerFactory.getLogger(BorgRepoConfigsRest.class);
-
-    /**
-     * @param id            id or name of repo.
-     * @param prettyPrinter If true then the json output will be in pretty format.
-     * @return {@link BorgRepoConfig} as json string.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    public String getRepoConfig(@QueryParam("id") String id, @QueryParam("prettyPrinter") boolean prettyPrinter) {
-        BorgRepoConfig repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(id);
-        return JsonUtils.toJson(repoConfig, prettyPrinter);
-    }
-
-    @POST
-    @Produces(MediaType.TEXT_PLAIN)
-    public void setRepoConfig(String jsonConfig) {
-        BorgRepoConfig newRepoConfig = JsonUtils.fromJson(BorgRepoConfig.class, jsonConfig);
-        if (newRepoConfig == null) {
-            log.error("Internal Rest error. Can't parse BorgRepoConfig: " + jsonConfig);
-            return;
-        }
-        if ("new".equals(newRepoConfig.getId())) {
-            newRepoConfig.setId(null);
-            ConfigurationHandler.getConfiguration().add(newRepoConfig);
-        } else if ("init".equals(newRepoConfig.getId())) {
-            newRepoConfig.setId(null);
-            ConfigurationHandler.getConfiguration().add(newRepoConfig);
-        } else {
-            BorgRepoConfig repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(newRepoConfig.getId());
-            if (repoConfig == null) {
-                log.error("Can't find repo config '" + newRepoConfig.getId() + "'. Can't save new settings.");
-                return;
-            }
-            ButlerCache.getInstance().clearRepoCacheAccess(repoConfig.getRepo());
-            ButlerCache.getInstance().clearRepoCacheAccess(newRepoConfig.getRepo());
-            repoConfig.copyFrom(newRepoConfig);
-        }
-        ConfigurationHandler.getInstance().save();
-    }
-
-    /**
-     * @param idOrName id or name of repo to remove from BorgButler.
-     * @return "OK" if removed or error string.
-     */
-    @GET
-    @Path("remove")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String removeRepoConfig(@QueryParam("id") String idOrName) {
-        boolean result = ConfigurationHandler.getConfiguration().remove(idOrName);
-        if (!result) {
-            String error = "Repo config with id or name '" + idOrName + "' not found. Can't remove the repo.";
-            log.error(error);
-            return error;
-        }
-        ConfigurationHandler.getInstance().save();
-        return "OK";
-    }
-
-    /**
-     * @param jsonRepoConfig All configuration value of the repo to check.
-     * @return Result of borg (tbd.).
-     */
-    @POST
-    @Path("check")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String checkConfig(String jsonRepoConfig) {
-        log.info("Testing repo config: " + jsonRepoConfig);
-        BorgRepoConfig repoConfig = JsonUtils.fromJson(BorgRepoConfig.class, jsonRepoConfig);
-        BorgCommandResult<Repository> result = BorgCommands.info(repoConfig);
-        return result.getStatus() == JobResult.Status.OK ? "OK" : result.getError();
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationInfo.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationInfo.java
deleted file mode 100644
index 7f41d55..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationInfo.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.server.BorgVersion;
-import de.micromata.borgbutler.server.ServerConfiguration;
-
-public class ConfigurationInfo {
-    private ServerConfiguration serverConfiguration;
-    private BorgVersion borgVersion;
-
-    public ServerConfiguration getServerConfiguration() {
-        return this.serverConfiguration;
-    }
-
-    public BorgVersion getBorgVersion() {
-        return this.borgVersion;
-    }
-
-    public ConfigurationInfo setServerConfiguration(ServerConfiguration serverConfiguration) {
-        this.serverConfiguration = serverConfiguration;
-        return this;
-    }
-
-    public ConfigurationInfo setBorgVersion(BorgVersion borgVersion) {
-        this.borgVersion = borgVersion;
-        return this;
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java
deleted file mode 100644
index a5b2ad4..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.cache.ButlerCache;
-import de.micromata.borgbutler.config.ConfigurationHandler;
-import de.micromata.borgbutler.json.JsonUtils;
-import de.micromata.borgbutler.server.BorgInstallation;
-import de.micromata.borgbutler.server.ServerConfiguration;
-import de.micromata.borgbutler.server.user.UserData;
-import de.micromata.borgbutler.server.user.UserManager;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.ws.rs.*;
-import javax.ws.rs.core.MediaType;
-
-@Path("/configuration")
-public class ConfigurationRest {
-    private Logger log = LoggerFactory.getLogger(ConfigurationRest.class);
-
-    /**
-     * @param prettyPrinter If true then the json output will be in pretty format.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Path("config")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String getConfig(@QueryParam("prettyPrinter") boolean prettyPrinter) {
-        ConfigurationInfo configurationInfo = new ConfigurationInfo();
-        configurationInfo.setServerConfiguration(ServerConfiguration.get());
-        configurationInfo.setBorgVersion(BorgInstallation.getInstance().getBorgVersion());
-        String json = JsonUtils.toJson(configurationInfo, prettyPrinter);
-        return json;
-    }
-
-    @POST
-    @Path("config")
-    @Produces(MediaType.TEXT_PLAIN)
-    public void setConfig(String jsonConfig) {
-        ConfigurationHandler configurationHandler = ConfigurationHandler.getInstance();
-        ConfigurationInfo configurationInfo = JsonUtils.fromJson(ConfigurationInfo.class, jsonConfig);
-        BorgInstallation.getInstance().configure(configurationInfo.getServerConfiguration(), configurationInfo.getBorgVersion().getBorgBinary());
-        ServerConfiguration configuration = ServerConfiguration.get();
-        configuration.copyFrom(configurationInfo.getServerConfiguration());
-        configurationHandler.save();
-    }
-
-    /**
-     * @param prettyPrinter If true then the json output will be in pretty format.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Path("user")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String getUser(@QueryParam("prettyPrinter") boolean prettyPrinter) {
-        UserData user = RestUtils.getUser();
-        String json = JsonUtils.toJson(user, prettyPrinter);
-        return json;
-    }
-
-    @POST
-    @Path("user")
-    @Produces(MediaType.TEXT_PLAIN)
-    public void setUser(String jsonConfig) {
-        UserData user = JsonUtils.fromJson(UserData.class, jsonConfig);
-        if (user.getLocale() != null && StringUtils.isBlank(user.getLocale().getLanguage())) {
-            // Don't set locale with "" as language.
-            user.setLocale(null);
-        }
-        if (StringUtils.isBlank(user.getDateFormat())) {
-            // Don't set dateFormat as "".
-            user.setDateFormat(null);
-        }
-        UserManager.instance().saveUser(user);
-    }
-
-    /**
-     * Resets the settings to default values (deletes all settings).
-     */
-    @GET
-    @Path("clearAllCaches")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String clearAllCaches() {
-        log.info("Clear all caches called...");
-        ButlerCache.getInstance().clearAllCaches();
-        return "OK";
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/FilesystemBrowserRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/FilesystemBrowserRest.java
deleted file mode 100644
index 5045ef2..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/FilesystemBrowserRest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.json.JsonUtils;
-import de.micromata.borgbutler.server.RunningMode;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.swing.*;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import java.awt.*;
-import java.io.File;
-
-@Path("/files")
-public class FilesystemBrowserRest {
-    private Logger log = LoggerFactory.getLogger(FilesystemBrowserRest.class);
-
-    /**
-     * Opens a directory browser or file browser on the desktop app and returns the chosen dir/file. Works only if Browser and Desktop app are running
-     * on the same host.
-     *
-     * @param current The current path of file. If not given the directory/file browser starts with the last used directory or user.home.
-     * @return The chosen directory path (absolute path).
-     */
-    @GET
-    @Path("/browse-local-filesystem")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String browseLocalFilesystem(@Context HttpServletRequest requestContext, @QueryParam("current") String current) {
-        String msg = RestUtils.checkLocalDesktopAvailable(requestContext);
-        if (msg != null) {
-            log.info(msg);
-            return msg;
-        }
-        if (fileDialog != null || fileChooser != null) {
-            log.warn("Cannot call already opened file choose twice. Close file chooser first.");
-            return "{\"directory\": \"\"}";
-        }
-        File file = null;
-        synchronized (FilesystemBrowserRest.class) {
-            if (frame == null) {
-                frame = new JFrame("BorgButler");
-                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
-                frame.setSize(300, 100);
-                frame.setResizable(false);
-                frame.setLocationRelativeTo(null);
-                frame.setBackground(Color.WHITE);
-                frame.getContentPane().setBackground(Color.WHITE);
-                JLabel label = new JLabel("Click for choosing directory...", SwingConstants.CENTER);
-                frame.add(label);
-            }
-            if (RunningMode.getOSType() == RunningMode.OSType.MAC_OS) {
-                // The JFileChooser will hang after several calls, use AWT file dialog instead for Mac OS:
-                System.setProperty("apple.awt.fileDialogForDirectories", "true");
-                frame.setAlwaysOnTop(true);
-                frame.setVisible(true);
-                try {
-                    fileDialog = new FileDialog(frame, "Choose a directory", FileDialog.LOAD);
-                    if (StringUtils.isNotBlank(current)) {
-                        fileDialog.setDirectory(current);
-                    }
-                    fileDialog.toFront();
-                    fileDialog.setVisible(true);
-                    String filename = fileDialog.getFile();
-                    String directory = fileDialog.getDirectory();
-                    frame.setVisible(false);
-                    if (filename == null) {
-                        return "";
-                    }
-                    file = new File(directory, filename);
-                    if (!file.isDirectory()) {
-                        file = new File(directory);
-                    }
-                } finally {
-                    fileDialog = null;
-                }
-            } else {
-                try {
-                    if (StringUtils.isNotBlank(current)) {
-                        fileChooser = new JFileChooser(current);
-                    } else {
-                        fileChooser = new JFileChooser();
-                    }
-                    fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
-                    frame.setVisible(true);
-                    frame.setAlwaysOnTop(true);
-                    int returnCode = fileChooser.showDialog(frame, "Choose");
-                    frame.setVisible(false);
-                    frame.setAlwaysOnTop(false);
-                    if (returnCode == JFileChooser.APPROVE_OPTION) {
-                        file = fileChooser.getSelectedFile();
-                    }
-                } finally {
-                    fileChooser = null;
-                }
-            }
-        }
-        String filename = file != null ? JsonUtils.toJson(file.getAbsolutePath()) : "";
-        String result = "{\"directory\":\"" + filename + "\"}";
-        return result;
-    }
-
-    /**
-     * @return OK, if the local desktop services such as open file browser etc. are available.
-     */
-    @GET
-    @Path("/local-fileservices-available")
-    @Produces(MediaType.TEXT_PLAIN)
-    public String browseLocalFilesystem(@Context HttpServletRequest requestContext) {
-        String msg = RestUtils.checkLocalDesktopAvailable(requestContext);
-        if (msg != null) {
-            log.info(msg);
-            return msg;
-        }
-        return "OK";
-    }
-
-    private static JFrame frame;
-    private static FileDialog fileDialog;
-    private static JFileChooser fileChooser;
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java
deleted file mode 100644
index b17c855..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.json.JsonUtils;
-import de.micromata.borgbutler.server.I18nClientMessages;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import java.util.Locale;
-import java.util.Map;
-
-@Path("/i18n")
-public class I18nRest {
-    private Logger log = LoggerFactory.getLogger(I18nRest.class);
-
-    /**
-     *
-     * @param requestContext For detecting the user's client locale.
-     * @param locale If not given, the client's language (browser) will be used.
-     * @param keysOnly If true, only the keys will be returned. Default is false.
-     * @param prettyPrinter If true then the json output will be in pretty format.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Path("list")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String getList(@Context HttpServletRequest requestContext, @QueryParam("prettyPrinter") boolean prettyPrinter,
-                          @QueryParam("keysOnly") boolean keysOnly, @QueryParam("locale") String locale) {
-        Locale localeObject;
-        if (StringUtils.isNotBlank(locale)) {
-            localeObject = new Locale(locale);
-        } else {
-            localeObject = RestUtils.getUserLocale(requestContext);
-        }
-        Map<String, String> translations = I18nClientMessages.getInstance().getAllMessages(localeObject, keysOnly);
-        String json = JsonUtils.toJson(translations, prettyPrinter);
-        return json;
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/JobsRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/JobsRest.java
deleted file mode 100644
index be744f0..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/JobsRest.java
+++ /dev/null
@@ -1,200 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.BorgJob;
-import de.micromata.borgbutler.BorgQueueExecutor;
-import de.micromata.borgbutler.config.BorgRepoConfig;
-import de.micromata.borgbutler.config.ConfigurationHandler;
-import de.micromata.borgbutler.jobs.AbstractJob;
-import de.micromata.borgbutler.json.JsonUtils;
-import de.micromata.borgbutler.json.borg.ProgressInfo;
-import de.micromata.borgbutler.server.rest.queue.JsonJob;
-import de.micromata.borgbutler.server.rest.queue.JsonJobQueue;
-import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import java.util.ArrayList;
-import java.util.List;
-
-@Path("/jobs")
-public class JobsRest {
-    private static Logger log = LoggerFactory.getLogger(JobsRest.class);
-
-    private static List<JsonJobQueue> testList, oldJobsTestList;
-
-    /**
-     * @param repo If given, only the job queue of the given repo will be returned.
-     * @param testMode If true, then a test job list is created.
-     * @param prettyPrinter If true then the json output will be in pretty format.
-     * @return Job queues as json string.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    public String getJobs(@QueryParam("repo") String repo,
-                          @QueryParam("testMode") boolean testMode,
-                          @QueryParam("oldJobs") boolean oldJobs,
-                          @QueryParam("prettyPrinter") boolean prettyPrinter) {
-        log.debug("getJobs repo=" + repo + ", oldJobs=" + oldJobs);
-        if (testMode) {
-            // Return dynamic test queue:
-            return returnTestList(oldJobs, prettyPrinter);
-        }
-        boolean validRepo = false;
-        if (StringUtils.isNotBlank(repo) && !"null".equals(repo) && !"undefined".equals(repo)) {
-            validRepo = true;
-        }
-        BorgQueueExecutor borgQueueExecutor = BorgQueueExecutor.getInstance();
-        List<JsonJobQueue> queueList = new ArrayList<>();
-        if (validRepo) { // Get only the queue of the given repo:
-            JsonJobQueue queue = getQueue(repo, oldJobs);
-            if (queue != null) {
-                queueList.add(queue);
-            }
-        } else { // Get all the queues (of all repos).
-            for (String rep : borgQueueExecutor.getRepos()) {
-                JsonJobQueue queue = getQueue(rep, oldJobs);
-                if (queue != null) {
-                    queueList.add(queue);
-                }
-            }
-        }
-        return JsonUtils.toJson(queueList, prettyPrinter);
-    }
-
-    private JsonJobQueue getQueue(String repo, boolean oldJobs) {
-        BorgQueueExecutor borgQueueExecutor = BorgQueueExecutor.getInstance();
-        BorgRepoConfig repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(repo);
-        if (repoConfig == null) {
-            return null;
-        }
-        List<BorgJob<?>> borgJobList = borgQueueExecutor.getJobListCopy(repoConfig, oldJobs);
-        if (CollectionUtils.isEmpty(borgJobList))
-            return null;
-        JsonJobQueue queue = new JsonJobQueue().setRepo(repoConfig.getDisplayName());
-        queue.setJobs(new ArrayList<>(borgJobList.size()));
-        for (BorgJob<?> borgJob : borgJobList) {
-            JsonJob job = new JsonJob(borgJob);
-            queue.getJobs().add(job);
-        }
-        return queue;
-    }
-
-    /**
-     * @param uniqueJobNumberString The id of the job to cancel.
-     */
-    @Path("/cancel")
-    @GET
-    public void cancelJob(@QueryParam("uniqueJobNumber") String uniqueJobNumberString) {
-        Long uniqueJobNumber = null;
-        try {
-            uniqueJobNumber = Long.parseLong(uniqueJobNumberString);
-        } catch (NumberFormatException ex) {
-            log.error("Can't cancel job, because unique job number couln't be parsed (long value expected): " + uniqueJobNumberString);
-            return;
-        }
-        BorgQueueExecutor.getInstance().cancelJob(uniqueJobNumber);
-    }
-
-    /**
-     * Only for test purposes and development.
-     *
-     * @param oldJobs
-     * @param prettyPrinter
-     * @return
-     */
-    private String returnTestList(boolean oldJobs, boolean prettyPrinter) {
-        List<JsonJobQueue> list = oldJobs ? oldJobsTestList : testList;
-        if (list == null) {
-            list = new ArrayList<>();
-            long uniqueJobNumber = 100000;
-            JsonJobQueue queue = new JsonJobQueue().setRepo("My Computer");
-            addTestJob(queue, "info", "my-macbook", 0, 2342, uniqueJobNumber++, oldJobs);
-            addTestJob(queue, "list", "my-macbook", -1, -1, uniqueJobNumber++, oldJobs);
-            list.add(queue);
-
-            queue = new JsonJobQueue().setRepo("My Server");
-            addTestJob(queue, "list", "my-server", 0, 1135821, uniqueJobNumber++, oldJobs);
-            addTestJob(queue, "info", "my-server", -1, -1, uniqueJobNumber++, oldJobs);
-            list.add(queue);
-            if (oldJobs) {
-                oldJobsTestList = list;
-            } else {
-                testList = list;
-            }
-        } else if (!oldJobs) {
-            for (JsonJobQueue jobQueue : list) {
-                for (JsonJob job : jobQueue.getJobs()) {
-                    if (job.getStatus() != AbstractJob.Status.RUNNING) continue;
-                    long current = job.getProgressInfo().getCurrent();
-                    long total = job.getProgressInfo().getTotal();
-                    if (StringUtils.startsWith(job.getProgressInfo().getMessage(), "Calculating")) {
-                        // Info is a faster operation:
-                        current += Math.random() * total / 5;
-                    } else {
-                        // than get the complete archive file list:
-                        current += Math.random() * total / 30;
-                    }
-                    if (current > total) {
-                        current = 0; // Reset to beginning.
-                    }
-                    job.getProgressInfo().setCurrent(current);
-                    if (job.getProgressText().startsWith("Calculating")) {
-                        job.getProgressInfo().setMessage("Calculating statistics...  " + Math.round(100 * current / total) + "%");
-                    }
-                    job.buildProgressText();
-                }
-            }
-        }
-        return JsonUtils.toJson(list, prettyPrinter);
-    }
-
-    /**
-     * Only for test purposes and development.
-     *
-     * @param queue
-     * @param operation
-     * @param host
-     * @param current
-     * @param total
-     * @return
-     */
-    private JsonJob addTestJob(JsonJobQueue queue, String operation, String host, long current, long total, long uniqueNumber, boolean oldJobs) {
-        ProgressInfo progressInfo = new ProgressInfo()
-                .setCurrent(current)
-                .setTotal(total);
-        JsonJob job = new JsonJob()
-                .setProgressInfo(progressInfo)
-                .setStatus(AbstractJob.Status.QUEUED);
-        if ("info".equals(operation)) {
-            progressInfo.setMessage("Calculating statistics... ");
-            job.setDescription("Loading info of archive '" + host + "-2018-12-05T23:10:33' of repo '" + queue.getRepo() + "'.")
-                    .setCommandLineAsString("borg info --json --log-json --progress ssh://...:23/./backups/" + host + "::" + host + "-2018-12-05T23:10:33");
-        } else {
-            progressInfo.setMessage("Getting file list... ");
-            job.setDescription("Loading list of files of archive '" + host + "-2018-12-05T17:30:38' of repo '" + queue.getRepo() + "'.")
-                    .setCommandLineAsString("borg list --json-lines ssh://...:23/./backups/" + host + "::" + host + "-2018-12-05T17:30:38");
-        }
-        job.buildProgressText();
-        if (current >= 0) {
-            job.setStatus(AbstractJob.Status.RUNNING);
-        } else {
-            job.setStatus(AbstractJob.Status.QUEUED);
-        }
-        if (queue.getJobs() == null) {
-            queue.setJobs(new ArrayList<>());
-        }
-        job.setUniqueJobNumber(uniqueNumber);
-        if (oldJobs) {
-            job.setStatus(uniqueNumber % 2 == 0 ? AbstractJob.Status.CANCELLED : AbstractJob.Status.DONE);
-        }
-        queue.getJobs().add(job);
-        return job;
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/LoggingRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/LoggingRest.java
deleted file mode 100644
index e268a36..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/LoggingRest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.json.JsonUtils;
-import de.micromata.borgbutler.server.logging.Log4jMemoryAppender;
-import de.micromata.borgbutler.server.logging.LogFilter;
-import de.micromata.borgbutler.server.logging.LogLevel;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-
-@Path("/logging")
-public class LoggingRest {
-    private Logger log = LoggerFactory.getLogger(LoggingRest.class);
-
-    /**
-     * @param requestContext
-     * @param search
-     * @param logLevelTreshold fatal, error, warn, info, debug or trace (case insensitive).
-     * @param maxSize          Max size of the result list.
-     * @param ascendingOrder   Default is false (default is descending order).
-     * @param lastReceivedOrderNumber The last received order number for updating log entries (preventing querying all entries again).
-     * @param prettyPrinter
-     * @return
-     */
-    @GET
-    @Path("query")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String query(@Context HttpServletRequest requestContext,
-                        @QueryParam("search") String search, @QueryParam("treshold") String logLevelTreshold,
-                        @QueryParam("maxSize") Integer maxSize, @QueryParam("ascendingOrder") Boolean ascendingOrder,
-                        @QueryParam("lastReceivedOrderNumber") Integer lastReceivedOrderNumber,
-                        @QueryParam("prettyPrinter") boolean prettyPrinter) {
-        LogFilter filter = new LogFilter();
-        filter.setSearch(search);
-        if (logLevelTreshold != null) {
-            try {
-                LogLevel treshold = LogLevel.valueOf(logLevelTreshold.trim().toUpperCase());
-                filter.setThreshold(treshold);
-            } catch (IllegalArgumentException ex) {
-                log.error("Can't parse log level treshold: " + logLevelTreshold + ". Supported values (case insensitive): " + LogLevel.getSupportedValues());
-            }
-        }
-        if (filter.getThreshold() == null) {
-            filter.setThreshold(LogLevel.INFO);
-        }
-        if (maxSize != null) {
-            filter.setMaxSize(maxSize);
-        }
-        if (ascendingOrder != null && ascendingOrder == true) {
-            filter.setAscendingOrder(true);
-        }
-        if (lastReceivedOrderNumber != null) {
-            filter.setLastReceivedLogOrderNumber(lastReceivedOrderNumber);
-        }
-        Log4jMemoryAppender appender = Log4jMemoryAppender.getInstance();
-        String json = JsonUtils.toJson(appender.query(filter, RestUtils.getUserLocale(requestContext)), prettyPrinter);
-        return json;
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ReposRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ReposRest.java
deleted file mode 100644
index 337dfb6..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ReposRest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.cache.ButlerCache;
-import de.micromata.borgbutler.data.Repository;
-import de.micromata.borgbutler.json.JsonUtils;
-import de.micromata.borgbutler.json.borg.BorgRepository;
-import org.apache.commons.collections4.CollectionUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import java.util.List;
-
-@Path("/repos")
-public class ReposRest {
-    private static Logger log = LoggerFactory.getLogger(ReposRest.class);
-
-    /**
-     *
-     * @param prettyPrinter If true then the json output will be in pretty format.
-     * @return A list of repositories of type {@link BorgRepository}.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Path("list")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String getList(@QueryParam("prettyPrinter") boolean prettyPrinter) {
-        List<Repository> repositories = ButlerCache.getInstance().getAllRepositories();
-        if (CollectionUtils.isEmpty(repositories)) {
-            return "[]";
-        }
-        return JsonUtils.toJson(repositories, prettyPrinter);
-    }
-
-    /**
-     *
-     * @param id id or name of repo.
-     * @param prettyPrinter If true then the json output will be in pretty format.
-     * @return {@link Repository} (without list of archives) as json string.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Path("repo")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String getRepo(@QueryParam("id") String id, @QueryParam("prettyPrinter") boolean prettyPrinter) {
-        Repository repository = ButlerCache.getInstance().getRepository(id);
-        return JsonUtils.toJson(repository, prettyPrinter);
-    }
-
-    /**
-     *
-     * @param id id or name of repo.
-     * @param prettyPrinter If true then the json output will be in pretty format.
-     * @return {@link Repository} (including list of archives) as json string.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Path("repoArchiveList")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String getRepoArchiveList(@QueryParam("id") String id, @QueryParam("force") boolean force,
-                                     @QueryParam("prettyPrinter") boolean prettyPrinter) {
-        if (force) {
-            Repository repo = ButlerCache.getInstance().getRepository(id);
-            ButlerCache.getInstance().clearRepoCacheAccess(repo);
-        }
-        Repository repository = ButlerCache.getInstance().getRepositoryArchives(id);
-        return JsonUtils.toJson(repository, prettyPrinter);
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/RestUtils.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/RestUtils.java
deleted file mode 100644
index a69231d..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/RestUtils.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.server.RunningMode;
-import de.micromata.borgbutler.server.user.UserData;
-import de.micromata.borgbutler.server.user.UserUtils;
-import org.slf4j.Logger;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.core.Response;
-import java.util.Locale;
-
-public class RestUtils {
-    /**
-     * @return null, if the local app (JavaFX) is running and the request is from localhost. Otherwise message, why local
-     * service isn't available.
-     */
-    public static String checkLocalDesktopAvailable(HttpServletRequest requestContext) {
-        if (RunningMode.getServerType() != RunningMode.ServerType.DESKTOP) {
-            return "Service unavailable. No desktop app on localhost available.";
-        }
-        String remoteAddr = requestContext.getRemoteAddr();
-        if (remoteAddr == null || !remoteAddr.equals("127.0.0.1")) {
-            return "Service not available. Can't call this service remote. Run this service on localhost of the running desktop app.";
-        }
-        return null;
-    }
-
-    /**
-     * @return Returns the user put by the UserFilter.
-     * @see UserUtils#getUser()
-     * @see de.micromata.borgbutler.server.user.UserFilter
-     */
-    static UserData getUser() {
-        UserData user = UserUtils.getUser();
-        if (user == null) {
-            throw new IllegalStateException("No user given in rest call.");
-        }
-        return UserUtils.getUser();
-    }
-
-    static Locale getUserLocale(HttpServletRequest requestContext) {
-        UserData user = RestUtils.getUser();
-        Locale locale = user.getLocale();
-        if (locale == null) {
-            locale = requestContext.getLocale();
-        }
-        return locale;
-    }
-
-    static Response get404Response(Logger log, String errorMessage) {
-        log.error(errorMessage);
-        Response response = Response.status(404).
-                entity(errorMessage).
-                type("text/plain").
-                build();
-        return response;
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfo.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfo.java
deleted file mode 100644
index 394730f..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfo.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.BorgQueueStatistics;
-import de.micromata.borgbutler.server.BorgVersion;
-
-/**
- * Statistics of all the job queues, especially the number of total queued and running jobs.
- * This is used e. g. by the client for showing a badge near to the menu entry "job monitor" with the number
- * of Jobs in the queues.
- */
-public class SystemInfo {
-    private BorgQueueStatistics queueStatistics;
-
-    private boolean configurationOK;
-
-    private BorgVersion borgVersion;
-
-    public BorgQueueStatistics getQueueStatistics() {
-        return this.queueStatistics;
-    }
-
-    public boolean isConfigurationOK() {
-        return this.configurationOK;
-    }
-
-    public BorgVersion getBorgVersion() {
-        return this.borgVersion;
-    }
-
-    public SystemInfo setQueueStatistics(BorgQueueStatistics queueStatistics) {
-        this.queueStatistics = queueStatistics;
-        return this;
-    }
-
-    public SystemInfo setConfigurationOK(boolean configurationOK) {
-        this.configurationOK = configurationOK;
-        return this;
-    }
-
-    public SystemInfo setBorgVersion(BorgVersion borgVersion) {
-        this.borgVersion = borgVersion;
-        return this;
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfoRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfoRest.java
deleted file mode 100644
index 6f8112c..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfoRest.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.BorgQueueExecutor;
-import de.micromata.borgbutler.json.JsonUtils;
-import de.micromata.borgbutler.server.BorgInstallation;
-import de.micromata.borgbutler.server.BorgVersion;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-
-@Path("/system")
-public class SystemInfoRest {
-    private static Logger log = LoggerFactory.getLogger(SystemInfoRest.class);
-
-    /**
-     * @return The total number of jobs queued or running (and other statistics): {@link de.micromata.borgbutler.BorgQueueStatistics}.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("info")
-    public String getStatistics() {
-        BorgVersion borgVersion = BorgInstallation.getInstance().getBorgVersion();
-        SystemInfo systemInfonfo = new SystemInfo()
-                .setQueueStatistics(BorgQueueExecutor.getInstance().getStatistics())
-                .setConfigurationOK(borgVersion.isVersionOK())
-                .setBorgVersion(borgVersion);
-        return JsonUtils.toJson(systemInfonfo);
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java
deleted file mode 100644
index 82f033f..0000000
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package de.micromata.borgbutler.server.rest;
-
-import de.micromata.borgbutler.json.JsonUtils;
-import de.micromata.borgbutler.server.Languages;
-import de.micromata.borgbutler.server.Version;
-import de.micromata.borgbutler.server.user.UserData;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import java.util.Date;
-import java.util.Locale;
-
-@Path("/")
-public class VersionRest {
-    private Logger log = LoggerFactory.getLogger(VersionRest.class);
-
-    /**
-     *
-     * @param requestContext For detecting the user's client locale.
-     * @param prettyPrinter If true then the json output will be in pretty format.
-     * @see JsonUtils#toJson(Object, boolean)
-     */
-    @GET
-    @Path("version")
-    @Produces(MediaType.APPLICATION_JSON)
-    public String getVersion(@Context HttpServletRequest requestContext, @QueryParam("prettyPrinter") boolean prettyPrinter) {
-        UserData user = RestUtils.getUser();
-        String language = Languages.asString(user.getLocale());
-        if (StringUtils.isBlank(language)) {
-            Locale locale = requestContext.getLocale();
-            language = locale.getLanguage();
-        }
-        MyVersion version = new MyVersion(language, RestUtils.checkLocalDesktopAvailable(requestContext) == null);
-        String json = JsonUtils.toJson(version, prettyPrinter);
-        return json;
-    }
-
-    public class MyVersion {
-        private Version version;
-        private String language;
-        private boolean localDesktopAvailable;
-
-        private MyVersion(String language, boolean localDesktopAvailable) {
-            this.version = Version.getInstance();
-            this.language = language;
-            this.localDesktopAvailable = localDesktopAvailable;
-        }
-
-        public String getAppName() {
-            return version.getAppName();
-        }
-
-        public String getVersion() {
-            return version.getVersion();
-        }
-
-        public String getBuildDateUTC() {
-            return version.getBuildDateUTC();
-        }
-
-        public Date getBuildDate() {
-            return version.getBuildDate();
-        }
-
-        /**
-         * @return Version of the available update, if exist. Otherwise null.
-         */
-        public String getUpdateVersion() {
-            return version.getUpdateVersion();
-        }
-
-        public String getLanguage() {
-            return language;
-        }
-
-        public boolean isLocalDesktopAvailable() {
-            return localDesktopAvailable;
-        }
-    }
-}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/SingleUserManager.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/SingleUserManager.java
index 0391b8d..233016d 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/SingleUserManager.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/SingleUserManager.java
@@ -48,7 +48,11 @@
         String dateFormat = userData.getDateFormat();
         this.singleUser.setDateFormat(dateFormat);
         String lang = Languages.asString(locale);
-        preferences.put(USER_LOCAL_PREF_KEY, lang);
+        if (lang != null) {
+            preferences.put(USER_LOCAL_PREF_KEY, lang);
+        } else {
+            preferences.remove(USER_LOCAL_PREF_KEY);
+        }
         try {
             preferences.flush();
         } catch (BackingStoreException ex) {
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserFilter.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserFilter.java
index 9e03d68..87ae77e 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserFilter.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserFilter.java
@@ -1,9 +1,12 @@
 package de.micromata.borgbutler.server.user;
 
+import de.micromata.borgbutler.server.rest.RequestLog;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
 
 import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;
 
 /**
@@ -12,6 +15,7 @@
  * <br>
  * For requests from remote (not localhost) an exception is thrown due to security reasons.
  */
+@Component
 public class UserFilter implements Filter {
     private Logger log = LoggerFactory.getLogger(UserFilter.class);
 
@@ -41,6 +45,7 @@
             userData = UserManager.instance().getUser("dummy");
             UserUtils.setUser(userData, request.getLocale());
             if (log.isDebugEnabled()) log.debug("Request for user: " + userData);
+            log.info("Request for user: " + userData + ": " + RequestLog.asString((HttpServletRequest) request));
             chain.doFilter(request, response);
         } finally {
             UserUtils.removeUser();
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/BorgButlerApplication.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/BorgButlerApplication.kt
new file mode 100644
index 0000000..f261fcd
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/BorgButlerApplication.kt
@@ -0,0 +1,179 @@
+package de.micromata.borgbutler.server
+
+import de.micromata.borgbutler.cache.ButlerCache
+import de.micromata.borgbutler.config.ConfigurationHandler.Companion.init
+import de.micromata.borgbutler.config.ConfigurationHandler.Companion.setConfigClazz
+import de.micromata.borgbutler.server.user.SingleUserManager
+import de.micromata.borgbutler.server.user.UserManager
+import mu.KotlinLogging
+import org.apache.commons.cli.*
+import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
+import org.apache.commons.io.FileUtils
+import org.apache.commons.io.FilenameUtils
+import org.apache.commons.lang3.StringUtils
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.SpringApplication
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.context.event.ApplicationReadyEvent
+import org.springframework.context.event.EventListener
+import java.awt.Desktop
+import java.io.*
+import java.net.URI
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.*
+import javax.annotation.PreDestroy
+
+private val log = KotlinLogging.logger {}
+
+@SpringBootApplication
+open class BorgButlerApplication {
+
+    @Value("\${server.address}")
+    private var serverAddress: String = "127.0.0.1"
+
+    @Value("\${server.port}")
+    private var serverPort = 9042
+
+    private fun _start(args: Array<out String>) {
+        setConfigClazz(ServerConfiguration::class.java)
+        // create Options object
+        val options = Options()
+        options.addOption(
+            "e",
+            "extract-archive-content",
+            true,
+            "Extracts the content of an archive cache file only (doesn't start the server). A complete file list of the archive will be extracted to stdout."
+        )
+        options.addOption("p", "port", true, "The default port for the web server.")
+        options.addOption("q", "quiet", false, "Don't open browser automatically.")
+        options.addOption("h", "help", false, "Print this help screen.")
+        //options.addOption("homeDir", true, "Specify own home directory of butler. Default is $HOME/.borgbutler");
+        val parser: CommandLineParser = DefaultParser()
+        try {
+            // parse the command line arguments
+            val line = parser.parse(options, args)
+            if (line.hasOption('h')) {
+                printHelp(options)
+                return
+            }
+            if (line.hasOption('e')) {
+                val file = line.getOptionValue("e")
+                printArchiveContent(file)
+                return
+            }
+            if (line.hasOption('p')) {
+                // initialise the member variable
+                val portString = line.getOptionValue("p")
+                try {
+                    val port = portString.toInt()
+                    if (port < 1 || port > 65535) {
+                        System.err.println("Port outside range.")
+                        return
+                    }
+                    ServerConfiguration.get().port = port
+                } catch (ex: NumberFormatException) {
+                    printHelp(options)
+                    return
+                }
+            }
+            val applicationHome = System.getProperty("borgbutlerHome")
+            if (applicationHome != null) {
+                init(applicationHome)
+            }
+            if (Desktop.isDesktopSupported()) {
+                RunningMode.setServerType(RunningMode.ServerType.DESKTOP)
+            } else {
+                RunningMode.setServerType(RunningMode.ServerType.SERVER)
+            }
+            RunningMode.logMode()
+
+            UserManager.setUserManager(SingleUserManager())
+
+            BorgInstallation.getInstance().initialize()
+
+            // 0.0.0.0 for Docker installations.
+            val url = "http://$serverAddress:$serverPort/".replace("0.0.0.0", "127.0.0.1")
+            if (!line.hasOption('q')) {
+                try {
+                    Desktop.getDesktop().browse(URI.create(url))
+                } catch (ex: Exception) {
+                    log.info("Can't open web browser: " + ex.message)
+                }
+            } else {
+                log.info("Please open your browser: $url")
+            }
+        } catch (ex: ParseException) {
+            // oops, something went wrong
+            System.err.println("Parsing failed.  Reason: " + ex.message)
+            printHelp(options)
+        }
+    }
+
+    @EventListener(ApplicationReadyEvent::class)
+    open fun startApp() {
+    }
+
+    @PreDestroy
+    open fun shutdownApp() {
+        log.info("Shutting down BorgButler web server...")
+        ButlerCache.getInstance().shutdown()
+    }
+
+    companion object {
+        private val main = BorgButlerApplication()
+
+        @JvmStatic
+        fun main(vararg args: String) {
+            main._start(args)
+            SpringApplication.run(BorgButlerApplication::class.java, *args)
+        }
+
+        private fun printHelp(options: Options) {
+            val formatter = HelpFormatter()
+            formatter.printHelp("borgbutler-server", options)
+        }
+
+        private fun printArchiveContent(fileName: String) {
+            val file = File(fileName)
+            val fileList = ButlerCache.getInstance().getArchiveContent(file)
+            var parseFormatExceptionPrinted = false
+            if (fileList != null && fileList.size > 0) {
+                val tz = TimeZone.getTimeZone("UTC")
+                val iso: DateFormat =
+                    SimpleDateFormat("yyyy-MM-dd HH:mm:ss'Z'") // Quoted "Z" to indicate UTC, no timezone offset
+                iso.timeZone = tz
+                val df: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S")
+                val out = File(FilenameUtils.getBaseName(fileName) + ".txt.gz")
+                log.info("Writing file list to: " + out.absolutePath)
+                try {
+                    PrintWriter(BufferedOutputStream(GzipCompressorOutputStream(FileOutputStream(out)))).use { writer ->
+                        for (item in fileList) {
+                            var time = item.mtime
+                            if (time.indexOf('T') > 0) {
+                                try {
+                                    val date = df.parse(item.mtime)
+                                    time = iso.format(date)
+                                } catch (ex: java.text.ParseException) {
+                                    if (!parseFormatExceptionPrinted) {
+                                        parseFormatExceptionPrinted = true
+                                        log.error("Can't parse date: " + item.mtime)
+                                    }
+                                }
+                            }
+                            writer.write(
+                                item.mode + " " + item.user + " "
+                                        + StringUtils.rightPad(FileUtils.byteCountToDisplaySize(item.size), 10)
+                                        + " " + time + " " + item.path
+                            )
+                            writer.write("\n")
+                        }
+                    }
+                } catch (ex: IOException) {
+                    log.error("Can't write file '" + out.absolutePath + "': " + ex.message)
+                }
+            }
+            // 2018-12-04T22:44:58.924642
+        }
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/ServerConfiguration.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/ServerConfiguration.kt
new file mode 100644
index 0000000..cbfcc81
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/ServerConfiguration.kt
@@ -0,0 +1,50 @@
+package de.micromata.borgbutler.server
+
+import de.micromata.borgbutler.config.Configuration
+import de.micromata.borgbutler.config.ConfigurationHandler.Companion.getConfiguration
+import mu.KotlinLogging
+import org.apache.commons.lang3.StringUtils
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Component
+
+private val log = KotlinLogging.logger {}
+
+class ServerConfiguration : Configuration() {
+    var port = WEBSERVER_PORT_DEFAULT
+
+    /**
+     * If true, CrossOriginFilter will be set.
+     */
+    var isWebDevelopmentMode = WEB_DEVELOPMENT_MODE_PREF_DEFAULT
+
+    fun copyFrom(other: ServerConfiguration) {
+        super.copyFrom(other)
+        port = other.port
+        isWebDevelopmentMode = other.isWebDevelopmentMode
+    }
+
+    companion object {
+        val supportedLanguages = arrayOf("en", "de")
+        const val WEBSERVER_PORT_DEFAULT = 9042
+        private const val WEB_DEVELOPMENT_MODE_PREF_DEFAULT = false
+        @JvmStatic
+        var applicationHome: String? = null
+            get() {
+                if (field == null) {
+                    field = System.getProperty("applicationHome")
+                    if (StringUtils.isBlank(field)) {
+                        field = System.getProperty("user.dir")
+                        log.info("applicationHome is not given as JVM   parameter. Using current working dir (OK for start in IDE): $field")
+                    }
+                }
+                return field
+            }
+            private set
+
+        @JvmStatic
+        fun get(): ServerConfiguration {
+            return getConfiguration() as ServerConfiguration
+        }
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/WebConfig.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/WebConfig.kt
new file mode 100644
index 0000000..b24752a
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/WebConfig.kt
@@ -0,0 +1,30 @@
+package de.micromata.borgbutler.server
+
+import mu.KotlinLogging
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.web.servlet.config.annotation.CorsRegistry
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
+
+
+private val log = KotlinLogging.logger {}
+
+@Configuration
+@EnableWebMvc
+open class WebConfig : WebMvcConfigurer {
+    override fun addCorsMappings(registry: CorsRegistry) {
+        if (ServerConfiguration.get().isWebDevelopmentMode) {
+            log.warn("*********************************")
+            log.warn("***********            **********")
+            log.warn("*********** ATTENTION! **********")
+            log.warn("***********            **********")
+            log.warn("*********** Running in **********")
+            log.warn("*********** dev mode!  **********")
+            log.warn("***********            **********")
+            log.warn("*********************************")
+            log.warn("Don't deliver this app in dev mode due to security reasons (CrossOriginFilter is set)!")
+            registry.addMapping("/**")
+        }
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/logging/LoggerMemoryAppender.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/logging/LoggerMemoryAppender.kt
new file mode 100644
index 0000000..cf5a57a
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/logging/LoggerMemoryAppender.kt
@@ -0,0 +1,134 @@
+package de.micromata.borgbutler.server.logging
+
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.AppenderBase
+import mu.KotlinLogging
+import org.apache.commons.lang3.StringUtils
+import java.util.*
+
+private val log = KotlinLogging.logger {}
+
+class LoggerMemoryAppender : AppenderBase<ILoggingEvent?>() {
+    private var lastLogEntryOrderNumber = -1
+
+    var queue = FiFoBuffer<LoggingEventData>(QUEUE_SIZE)
+
+    override fun append(event: ILoggingEvent?) {
+        val eventData = LoggingEventData(event)
+        eventData.orderNumber = ++lastLogEntryOrderNumber
+        queue.add(eventData)
+    }
+
+    /**
+     * For testing purposes.
+     *
+     * @param event
+     */
+    fun append(event: LoggingEventData) {
+        queue.add(event)
+    }
+
+    fun query(filter: LogFilter?, locale: Locale?): List<LoggingEventData> {
+        val result: MutableList<LoggingEventData> = ArrayList()
+        if (filter == null) {
+            return result
+        }
+        var maxSize = if (filter.maxSize != null) filter.maxSize else MAX_RESULT_SIZE
+        if (maxSize > MAX_RESULT_SIZE) {
+            maxSize = MAX_RESULT_SIZE
+        }
+        var counter = 0
+        //I18n i18n = CoreI18n.getDefault().get(locale);
+        if (filter.isAscendingOrder) {
+            for (i in 0 until queue.size) {
+                val resultEvent = getResultEvent(filter, queue[i], locale) ?: continue
+                result.add(resultEvent)
+                if (++counter > maxSize) break
+            }
+        } else {
+            for (i in queue.size downTo 0) {
+                val resultEvent = getResultEvent(filter, queue[i], locale) ?: continue
+                result.add(resultEvent)
+                if (++counter > maxSize) break
+            }
+        }
+        return result
+    }
+
+    private fun getResultEvent(filter: LogFilter, event: LoggingEventData?, locale: Locale?): LoggingEventData? {
+        if (event == null) {
+            return null
+        }
+        if (!event.getLevel().matches(filter.threshold)) {
+            return null
+        }
+        if (filter.lastReceivedLogOrderNumber != null) {
+            if (event.getOrderNumber() <= filter.lastReceivedLogOrderNumber) {
+                return null
+            }
+        }
+        var logString: String? = null
+        val message = event.getMessage()
+        val localizedMessage = false
+        /*if (message != null && message.startsWith("i18n=")) {
+                I18nLogEntry i18nLogEntry = I18nLogEntry.parse(message);
+                message = i18n.formatMessage(i18nLogEntry.getI18nKey(), (Object[])i18nLogEntry.getArgs());
+                localizedMessage = true;
+            }*/if (StringUtils.isNotBlank(filter.search)) {
+            val sb = StringBuilder()
+            sb.append(event.logDate)
+            append(sb, event.getLevel(), true)
+            append(sb, message, true)
+            append(sb, event.getJavaClass(), true)
+            append(sb, event.stackTrace, filter.isShowStackTraces)
+            logString = sb.toString()
+        }
+        if (logString == null || matches(logString, filter.search)) {
+            var resultEvent: LoggingEventData = event
+            if (localizedMessage) {
+                // Need a clone
+                resultEvent = event.clone()
+                resultEvent.setMessage(message)
+            }
+            return resultEvent
+        }
+        return null
+    }
+
+    private fun append(sb: StringBuilder, value: Any?, append: Boolean) {
+        if (!append || value == null) {
+            return
+        }
+        sb.append("|#|").append(value)
+    }
+
+    private fun matches(str: String, searchString: String): Boolean {
+        if (StringUtils.isBlank(str)) {
+            return StringUtils.isBlank(searchString)
+        }
+        return if (StringUtils.isBlank(searchString)) {
+            true
+        } else str.toLowerCase().contains(searchString.toLowerCase())
+    }
+
+    companion object {
+        private const val MAX_RESULT_SIZE = 1000
+        private const val QUEUE_SIZE = 10000
+        private var instance: LoggerMemoryAppender? = null
+
+        fun getInstance(): LoggerMemoryAppender {
+            return instance!!
+        }
+    }
+
+    /**
+     * Initialized by logback on start-up (see logback-spring.xml).
+     */
+    init {
+        if (instance != null) {
+            log.warn { "*** LoggerMemoryAppender instantiated twice! Shouldn't occur. ***" }
+        } else {
+            instance = this
+        }
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ArchivesRest.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ArchivesRest.kt
new file mode 100644
index 0000000..b51921b
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ArchivesRest.kt
@@ -0,0 +1,221 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.BorgCommands
+import de.micromata.borgbutler.cache.ButlerCache
+import de.micromata.borgbutler.config.BorgRepoConfig
+import de.micromata.borgbutler.config.ConfigurationHandler
+import de.micromata.borgbutler.data.Archive
+import de.micromata.borgbutler.data.DiffFileSystemFilter
+import de.micromata.borgbutler.data.FileSystemFilter
+import de.micromata.borgbutler.json.JsonUtils
+import de.micromata.borgbutler.json.borg.BorgFilesystemItem
+import de.micromata.borgbutler.utils.DirUtils
+import mu.KotlinLogging
+import org.apache.commons.collections4.CollectionUtils
+import org.apache.commons.lang3.StringUtils
+import org.apache.commons.lang3.math.NumberUtils
+import org.apache.coyote.Response
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+import java.awt.Desktop
+import java.io.File
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+
+private val log = KotlinLogging.logger {}
+
+@RestController
+@RequestMapping("/rest/archives")
+class ArchivesRest {
+    /**
+     * @param repoName      Name of repository ([Repository.getName].
+     * @param archiveId     Id or name of archive.
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @return Repository (including list of archives) as json string.
+     * @see JsonUtils.toJson
+     */
+    @GetMapping
+    fun getArchive(
+        @RequestParam("repo") repoName: String,
+        @RequestParam("archiveId") archiveId: String,
+        @RequestParam("force", required = false) force: Boolean,
+        @RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean
+    ): String {
+        val archive: Archive = ButlerCache.getInstance().getArchive(repoName, archiveId, force == true)
+        if (force == true) {
+            ButlerCache.getInstance().deleteCachedArchiveContent(repoName, archiveId)
+        }
+        return JsonUtils.toJson(archive, prettyPrinter == true)
+    }
+
+    /**
+     * @param archiveId                     Id or name of archive.
+     * @param searchString                  The string to search for (key words separated by white chars, trailing ! char represents exclude).
+     * @param mode                          Flat (default) or tree.
+     * @param currentDirectory              The current displayed directory (only files and directories contained will be returned).
+     * @param maxResultSize                 maximum number of file items to return (default is 50).
+     * @param diffArchiveId                 If given, the differences between archiveId and diffArchiveId will be returned.
+     * @param autoChangeDirectoryToLeafItem If given, this method will step automatically into single sub directories.
+     * @param force                         If false (default), non cached file lists will not be loaded by borg.
+     * @param prettyPrinter                 If true then the json output will be in pretty format.
+     * @return Repository (including list of archives) as json string.
+     * @see JsonUtils.toJson
+     */
+    @GetMapping("filelist")
+    fun getArchiveFileList(
+        @RequestParam("archiveId") archiveId: String,
+        @RequestParam("searchString", required = false) searchString: String?,
+        @RequestParam("mode", required = false) mode: String?,
+        @RequestParam("currentDirectory", required = false) currentDirectory: String?,
+        @RequestParam("maxResultSize", required = false) maxResultSize: String?,
+        @RequestParam("diffArchiveId", required = false) diffArchiveId: String?,
+        @RequestParam("autoChangeDirectoryToLeafItem", required = false) autoChangeDirectoryToLeafItem: Boolean?,
+        @RequestParam("force", required = false) force: Boolean?,
+        @RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean?
+    ): String {
+        val diffMode = StringUtils.isNotBlank(diffArchiveId)
+        val maxSize = NumberUtils.toInt(maxResultSize, 50)
+        val filter = if (diffMode) DiffFileSystemFilter() else FileSystemFilter()
+        filter.setSearchString(searchString)
+            .setCurrentDirectory(currentDirectory)
+            .setAutoChangeDirectoryToLeafItem(autoChangeDirectoryToLeafItem == true)
+        var items: List<BorgFilesystemItem>?
+        if (diffMode) {
+            filter.setMode(FileSystemFilter.Mode.FLAT)
+            items = ButlerCache.getInstance().getArchiveContent(archiveId, true, filter)
+            val diffItems: List<BorgFilesystemItem> = ButlerCache.getInstance().getArchiveContent(
+                diffArchiveId, true,
+                filter
+            )
+            filter.setMaxResultSize(maxSize)
+                .setMode(mode)
+            items = (filter as DiffFileSystemFilter).extractDifferences(items, diffItems)
+            items = filter.reduce(items)
+        } else {
+            filter.setMode(mode)
+                .setMaxResultSize(maxSize)
+            // Get file list (without running diff).
+            items = ButlerCache.getInstance().getArchiveContent(
+                archiveId, force == true,
+                filter
+            )
+            if (items == null) {
+                return "[{\"mode\": \"notLoaded\"}]"
+            }
+        }
+        return JsonUtils.toJson(items, prettyPrinter == true)
+    }
+
+    /**
+     * @param archiveId
+     * @param openDownloads
+     * @param fileNumber    The fileNumber of the file or directory in the archive served by BorgButler's
+     */
+    @GetMapping("/restore")
+    fun restore(
+        @RequestParam("archiveId") archiveId: String,
+        @RequestParam("openDownloads", required = false) openDownloads: Boolean?,
+        @RequestParam("fileNumber") fileNumber: Int?
+    ): ResponseEntity<*> {
+        log.info("Requesting file #$fileNumber of archive '$archiveId'.")
+        val filter: FileSystemFilter = FileSystemFilter().setFileNumber(fileNumber)
+        val items: List<BorgFilesystemItem> = ButlerCache.getInstance().getArchiveContent(
+            archiveId, false,
+            filter
+        )
+        if (CollectionUtils.isEmpty(items)) {
+            log.error(
+                "Requested file #" + fileNumber + " not found in archive '" + archiveId
+                        + ". (May-be the archive content isn't yet loaded to the cache."
+            )
+            return RestUtils.notFound()
+        }
+        if (items.size != 1) {
+            log.error(
+                "Requested file #" + fileNumber + " found multiple times (" + items.size + ") in archive '" + archiveId
+                        + "! Please remove the archive files (may-be corrupted)."
+            )
+            return RestUtils.notFound()
+        }
+        val archive: Archive = ButlerCache.getInstance().getArchive(archiveId) ?: return RestUtils.notFound()
+
+        val repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(archive.repoId)
+        try {
+            val item: BorgFilesystemItem = items[0]
+            val restoreHomeDir: File = ConfigurationHandler.getConfiguration().getRestoreHomeDir()
+            val restoreDir: File = BorgCommands.extractFiles(restoreHomeDir, repoConfig, archive, item.getPath())
+            val files: List<Path?> = DirUtils.listFiles(restoreDir.toPath())
+            if (CollectionUtils.isEmpty(files)) {
+                log.error("No files extracted.")
+                return RestUtils.notFound()
+            }
+            if (openDownloads == true) openFileBrowser(File(restoreDir, item.getPath()))
+            return ResponseEntity.ok("OK")
+        } catch (ex: IOException) {
+            log.error("No file extracted: " + ex.message, ex)
+            return RestUtils.notFound()
+        }
+    }
+
+    private fun openFileBrowser(fileDirectory: File) {
+        if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) {
+            var file: File? = fileDirectory
+            if (!fileDirectory.exists() || Files.isSymbolicLink(fileDirectory.toPath())) {
+                // Open parent.
+                file = fileDirectory.parentFile
+            }
+            Desktop.getDesktop().browseFileDirectory(file)
+        }
+    }
+
+    private fun handleRestoredFiles(repoConfig: BorgRepoConfig, archive: Archive): Response? {
+        // Todo: Handle download of single files as well as download of zip archive (if BorgButler runs remote).
+        return null
+        /* File file = path.toFile();
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            FileUtils.copyFile(file, baos);
+        } catch (IOException ex) {
+            log.error(ex.getMessage(), ex);
+        }
+        BorgFilesystemItem item = items.get(0);
+        file = new File(item.getPath());
+        byte[] byteArray = baos.toByteArray();//result.getAsByteArrayOutputStream().toByteArray();
+        Response.ResponseBuilder builder = Response.ok(byteArray);
+        builder.header("Content-Disposition", "attachment; filename=" + file.getName());
+        // Needed to get the Content-Disposition by client:
+        builder.header("Access-Control-Expose-Headers", "Content-Disposition");
+        Response response = builder.build();
+        return response;
+
+        try {
+            //java.nio.file.Path tempDirWithPrefix = Files.createTempDirectory("borgbutler-extract-");
+            File restoreHomeDir = ConfigurationHandler.getConfiguration().getRestoreHomeDir();
+            File restoreDir = BorgCommands.extractFiles(restoreHomeDir, repoConfig, archive.getName(), item.getPath());
+            openFileBrowser(restoreDir);
+            List<java.nio.file.Path> files = DirUtils.listFiles(tempDir);
+            if (CollectionUtils.isEmpty(files)) {
+                log.error("No file extracted.");
+                Response.ResponseBuilder builder = Response.status(404);
+                return builder.build();
+            }
+            path = files.get(0);
+        } catch (IOException ex) {
+            log.error("No file extracted: " + ex.getMessage(), ex);
+            Response.ResponseBuilder builder = Response.status(404);
+            return builder.build();
+        } finally {
+           if (tempDir != null) {
+                try {
+                    FileUtils.deleteDirectory(tempDir.toFile());
+                } catch (IOException ex) {
+                    log.error("Error while trying to delete temporary directory '" + tempDir.toString() + "': " + ex.getMessage(), ex);
+                }
+            }
+        }*/
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/BorgRepoConfigsRest.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/BorgRepoConfigsRest.kt
new file mode 100644
index 0000000..f392664
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/BorgRepoConfigsRest.kt
@@ -0,0 +1,82 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.BorgCommandResult
+import de.micromata.borgbutler.BorgCommands
+import de.micromata.borgbutler.cache.ButlerCache
+import de.micromata.borgbutler.config.BorgRepoConfig
+import de.micromata.borgbutler.config.ConfigurationHandler
+import de.micromata.borgbutler.data.Repository
+import de.micromata.borgbutler.jobs.JobResult
+import de.micromata.borgbutler.json.JsonUtils
+import mu.KotlinLogging
+import org.springframework.web.bind.annotation.*
+
+private val log = KotlinLogging.logger {}
+
+@RestController
+@RequestMapping("/rest/repoConfig")
+class BorgRepoConfigsRest {
+    /**
+     * @param id            id or name of repo.
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @return [BorgRepoConfig] as json string.
+     * @see JsonUtils.toJson
+     */
+    @GetMapping
+    fun getRepoConfig(
+        @RequestParam("id") id: String,
+        @RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean?
+    ): String {
+        val repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(id)
+        return JsonUtils.toJson(repoConfig, prettyPrinter == true)
+    }
+
+    @PostMapping
+    fun setRepoConfig(@RequestBody newRepoConfig: BorgRepoConfig) {
+        if ("new" == newRepoConfig.getId()) {
+            newRepoConfig.setId(null)
+            ConfigurationHandler.getConfiguration().add(newRepoConfig)
+        } else if ("init" == newRepoConfig.getId()) {
+            newRepoConfig.setId(null)
+            ConfigurationHandler.getConfiguration().add(newRepoConfig)
+        } else {
+            val repoConfig: BorgRepoConfig? =
+                ConfigurationHandler.getConfiguration().getRepoConfig(newRepoConfig.getId())
+            if (repoConfig == null) {
+                log.error("Can't find repo config '" + newRepoConfig.getId() + "'. Can't save new settings.")
+                return
+            }
+            ButlerCache.getInstance().clearRepoCacheAccess(repoConfig.getRepo())
+            ButlerCache.getInstance().clearRepoCacheAccess(newRepoConfig.getRepo())
+            repoConfig.copyFrom(newRepoConfig)
+        }
+        ConfigurationHandler.getInstance().save()
+    }
+
+    /**
+     * @param idOrName id or name of repo to remove from BorgButler.
+     * @return "OK" if removed or error string.
+     */
+    @GetMapping("remove")
+    fun removeRepoConfig(@RequestParam("id") idOrName: String): String {
+        val result: Boolean = ConfigurationHandler.getConfiguration().remove(idOrName)
+        if (!result) {
+            val error = "Repo config with id or name '$idOrName' not found. Can't remove the repo."
+            log.error(error)
+            return error
+        }
+        ConfigurationHandler.getInstance().save()
+        return "OK"
+    }
+
+    /**
+     * @param jsonRepoConfig All configuration value of the repo to check.
+     * @return Result of borg (tbd.).
+     */
+    @PostMapping("check")
+    fun checkConfig(@RequestBody repoConfig: BorgRepoConfig): String {
+        log.info("Testing repo config: $repoConfig")
+        val result: BorgCommandResult<Repository> = BorgCommands.info(repoConfig)
+        return if (result.getStatus() == JobResult.Status.OK) "OK" else result.getError()
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ConfigurationInfo.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ConfigurationInfo.kt
new file mode 100644
index 0000000..a5e0b8f
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ConfigurationInfo.kt
@@ -0,0 +1,9 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.server.BorgVersion
+import de.micromata.borgbutler.server.ServerConfiguration
+
+class ConfigurationInfo(
+    var serverConfiguration: ServerConfiguration? = null,
+    var borgVersion: BorgVersion? = null
+)
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ConfigurationRest.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ConfigurationRest.kt
new file mode 100644
index 0000000..c0ac4b2
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ConfigurationRest.kt
@@ -0,0 +1,76 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.cache.ButlerCache
+import de.micromata.borgbutler.config.ConfigurationHandler
+import de.micromata.borgbutler.json.JsonUtils
+import de.micromata.borgbutler.server.BorgInstallation
+import de.micromata.borgbutler.server.ServerConfiguration
+import de.micromata.borgbutler.server.user.UserData
+import de.micromata.borgbutler.server.user.UserManager
+import mu.KotlinLogging
+import org.apache.commons.lang3.StringUtils
+import org.springframework.web.bind.annotation.*
+
+private val log = KotlinLogging.logger {}
+
+@RestController
+@RequestMapping("/rest/configuration")
+class ConfigurationRest {
+
+    /**
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @see JsonUtils.toJson
+     */
+    @GetMapping("config")
+    fun getConfig(@RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean?): String {
+        val configurationInfo = ConfigurationInfo()
+        configurationInfo.serverConfiguration = ServerConfiguration.get()
+        configurationInfo.borgVersion = BorgInstallation.getInstance().getBorgVersion()
+        return JsonUtils.toJson(configurationInfo, prettyPrinter)
+    }
+
+    @PostMapping("config")
+    fun setConfig(@RequestBody configurationInfo: ConfigurationInfo) {
+        val configurationHandler = ConfigurationHandler.getInstance()
+        BorgInstallation.getInstance()
+            .configure(configurationInfo.serverConfiguration, configurationInfo.borgVersion?.borgBinary)
+        val configuration: ServerConfiguration = ServerConfiguration.get()
+        configurationInfo.serverConfiguration?.let {
+            configuration.copyFrom(it)
+        }
+        configurationHandler.save()
+    }
+
+    /**
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @see JsonUtils.toJson
+     */
+    @GetMapping("user")
+    fun getUser(@RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean?): String {
+        val user: UserData = RestUtils.getUser()
+        return JsonUtils.toJson(user, prettyPrinter)
+    }
+
+    @PostMapping("user")
+    fun setUser(@RequestBody user: UserData) {
+        if (user.getLocale() != null && StringUtils.isBlank(user.getLocale().getLanguage())) {
+            // Don't set locale with "" as language.
+            user.setLocale(null)
+        }
+        if (StringUtils.isBlank(user.getDateFormat())) {
+            // Don't set dateFormat as "".
+            user.setDateFormat(null)
+        }
+        UserManager.instance().saveUser(user)
+    }
+
+    /**
+     * Resets the settings to default values (deletes all settings).
+     */
+    @GetMapping("clearAllCaches")
+    fun clearAllCaches(): String {
+        log.info("Clear all caches called...")
+        ButlerCache.getInstance().clearAllCaches()
+        return "OK"
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ExceptionStackTracePrinter.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ExceptionStackTracePrinter.kt
new file mode 100644
index 0000000..9c20d1a
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ExceptionStackTracePrinter.kt
@@ -0,0 +1,72 @@
+/////////////////////////////////////////////////////////////////////////////
+//
+// Project ProjectForge Community Edition
+//         www.projectforge.org
+//
+// Copyright (C) 2001-2021 Micromata GmbH, Germany (www.micromata.com)
+//
+// ProjectForge is dual-licensed.
+//
+// This community edition is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as published
+// by the Free Software Foundation; version 3 of the License.
+//
+// This community edition 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 General
+// Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see http://www.gnu.org/licenses/.
+//
+/////////////////////////////////////////////////////////////////////////////
+
+package de.micromata.borgbutler.server.rest
+
+/**
+ * Prints stack-trace without foreign packages in much shorter form than log.error(ex.message, ex) does.
+ * @author Kai Reinhard (k.reinhard@micromata.de)
+ */
+object ExceptionStackTracePrinter {
+
+    /**
+     * @param showExceptionMessage If true, the exception message itself will be prepended to stack trace.
+     * @param stopBeforeForeignPackages If true, after showing stack trace elements of own package 'org.projectforge' any further element is hidden if foreign stack element is reached.
+     * @param depth Maximum depth of stack trace elements to show (default is 10).
+     * @param showPackagesOnly Only stack elements started with one of these string is classified as own package to print. If nothing given, all 'org.projectforge' classes are
+     * classified as own packages.
+     */
+    @JvmStatic
+    @JvmOverloads
+    fun toString(ex: Exception, showExceptionMessage: Boolean = true, stopBeforeForeignPackages: Boolean = true, depth: Int = 10, vararg showPackagesOnly: String): String {
+        val sb = StringBuilder()
+        if (showExceptionMessage) {
+            sb.append(ex::class.java.name).append(":").append(ex.message).append("\n")
+        }
+        var counter = 0
+        val showPackages = if (showPackagesOnly.isEmpty()) OWN_PACKAGES else showPackagesOnly
+        var placeHolderPrinted = false
+        var ownStackelementsPrinted = false
+        for (element in ex.stackTrace) {
+            if (!showPackages.any { element.className.startsWith(it) }) {
+                if (ownStackelementsPrinted && stopBeforeForeignPackages) {
+                    sb.append("...following foreign packages are hidden...\n")
+                    break
+                }
+                if (!placeHolderPrinted) {
+                    sb.append("...(foreign packages are hidden)...\n")
+                    placeHolderPrinted = true
+                }
+                continue // Don't show foreign class entries.
+            }
+            ownStackelementsPrinted = true
+            sb.append("at ${element.className}.${element.methodName} (${element.fileName}:${element.lineNumber})\n")
+            if (++counter >= depth) {
+                break
+            }
+        }
+        return sb.toString()
+    }
+
+    val OWN_PACKAGES = arrayOf("de.micromata.borgbutler")
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/FilesystemBrowserRest.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/FilesystemBrowserRest.kt
new file mode 100644
index 0000000..afa4896
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/FilesystemBrowserRest.kt
@@ -0,0 +1,141 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.json.JsonUtils
+import de.micromata.borgbutler.server.RunningMode
+import mu.KotlinLogging
+import org.apache.commons.lang3.StringUtils
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+import java.awt.Color
+import java.awt.FileDialog
+import java.io.File
+import javax.servlet.http.HttpServletRequest
+import javax.swing.JFileChooser
+import javax.swing.JFrame
+import javax.swing.JLabel
+import javax.swing.SwingConstants
+
+private val log = KotlinLogging.logger {}
+
+@RestController
+@RequestMapping("/rest/files")
+class FilesystemBrowserRest {
+
+    /**
+     * Opens a directory browser or file browser on the desktop app and returns the chosen dir/file. Works only if Browser and Desktop app are running
+     * on the same host.
+     *
+     * @param current The current path of file. If not given the directory/file browser starts with the last used directory or user.home.
+     * @return The chosen directory path (absolute path).
+     */
+    @GetMapping("/browse-local-filesystem")
+    fun browseLocalFilesystem(
+        request: HttpServletRequest,
+        @RequestParam("current", required = false) current: String?
+    ): String {
+        val msg = RestUtils.checkLocalDesktopAvailable(request)
+        if (msg != null) {
+            log.info(msg)
+            return msg
+        }
+        if (fileDialog != null || fileChooser != null) {
+            log.warn("Cannot call already opened file choose twice. Close file chooser first.")
+            return "{\"directory\": \"\"}"
+        }
+        var file: File? = null
+        synchronized(FilesystemBrowserRest::class.java) {
+            if (frame == null) {
+                val fr = JFrame("BorgButler")
+                frame = fr
+                fr.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
+                fr.setSize(300, 100)
+                fr.setResizable(false)
+                fr.setLocationRelativeTo(null)
+                fr.setBackground(Color.WHITE)
+                fr.getContentPane().setBackground(Color.WHITE)
+                val label = JLabel("Click for choosing directory...", SwingConstants.CENTER)
+                fr.add(label)
+            }
+            if (RunningMode.getOSType() == RunningMode.OSType.MAC_OS) {
+                // The JFileChooser will hang after several calls, use AWT file dialog instead for Mac OS:
+                System.setProperty("apple.awt.fileDialogForDirectories", "true")
+                frame?.let {
+                    it.setAlwaysOnTop(true)
+                    it.setVisible(true)
+                }
+                try {
+                    val dialog =
+                        FileDialog(frame, "Choose a directory", FileDialog.LOAD)
+                    fileDialog = dialog
+                    if (StringUtils.isNotBlank(current)) {
+                        dialog.setDirectory(current)
+                    }
+                    dialog.toFront()
+                    dialog.setVisible(true)
+                    val filename: String? = dialog.getFile()
+                    val directory: String? = dialog.getDirectory()
+                    dialog.setVisible(false)
+                    if (filename == null) {
+                        return ""
+                    }
+                    file = File(directory, filename)
+                    if (file?.isDirectory != true) {
+                        file = File(directory)
+                    }
+                } finally {
+                    fileDialog = null
+                }
+            } else {
+                try {
+                    val chooser = if (StringUtils.isNotBlank(current)) {
+                        JFileChooser(current)
+                    } else {
+                        JFileChooser()
+                    }
+                    fileChooser = chooser
+                    chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
+                    frame?.let {
+                        it.setVisible(true)
+                        it.setAlwaysOnTop(true)
+                    }
+                    val returnCode: Int = chooser.showDialog(
+                        frame,
+                        "Choose"
+                    )
+                    frame?.let {
+                        it.setVisible(false)
+                        it.setAlwaysOnTop(false)
+                    }
+                    if (returnCode == JFileChooser.APPROVE_OPTION) {
+                        file = chooser.getSelectedFile()
+                    }
+                } finally {
+                    fileChooser = null
+                }
+            }
+        }
+        val filename = if (file != null) JsonUtils.toJson(file!!.absolutePath) else ""
+        return "{\"directory\":\"$filename\"}"
+    }
+
+    /**
+     * @return OK, if the local desktop services such as open file browser etc. are available.
+     */
+    @GetMapping("/local-fileservices-available")
+    fun browseLocalFilesystem(request: HttpServletRequest): String {
+        val msg = RestUtils.checkLocalDesktopAvailable(request)
+        if (msg != null) {
+            log.info(msg)
+            return msg
+        }
+        return "OK"
+    }
+
+    companion object {
+        private var frame: JFrame? = null
+        private var fileDialog: FileDialog? = null
+        private var fileChooser: JFileChooser? = null
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/GlobalDefaultExceptionHandling.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/GlobalDefaultExceptionHandling.kt
new file mode 100644
index 0000000..4681483
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/GlobalDefaultExceptionHandling.kt
@@ -0,0 +1,57 @@
+/////////////////////////////////////////////////////////////////////////////
+//
+// Project ProjectForge Community Edition
+//         www.projectforge.org
+//
+// Copyright (C) 2001-2021 Micromata GmbH, Germany (www.micromata.com)
+//
+// ProjectForge is dual-licensed.
+//
+// This community edition is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as published
+// by the Free Software Foundation; version 3 of the License.
+//
+// This community edition 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 General
+// Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see http://www.gnu.org/licenses/.
+//
+/////////////////////////////////////////////////////////////////////////////
+
+package de.micromata.borgbutler.server.rest
+
+import mu.KotlinLogging
+import org.springframework.core.annotation.AnnotationUtils
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.ControllerAdvice
+import org.springframework.web.bind.annotation.ExceptionHandler
+import org.springframework.web.bind.annotation.ResponseStatus
+import javax.servlet.http.HttpServletRequest
+
+private val log = KotlinLogging.logger {}
+
+@ControllerAdvice
+internal class GlobalDefaultExceptionHandler {
+    @ExceptionHandler(value = [(Exception::class)])
+    @Throws(Exception::class)
+    fun defaultErrorHandler(request: HttpServletRequest, ex: Exception): Any {
+        // If the exception is annotated with @ResponseStatus rethrow it and let
+        // the framework handle it.
+        if (AnnotationUtils.findAnnotation(ex.javaClass, ResponseStatus::class.java) != null) {
+            throw ex
+        }
+        log.error(
+            "Exception while processing request: ${ex.message} Request: ${RequestLog.asJson(request)},\nexception=${ex.message}\n${
+                ExceptionStackTracePrinter.toString(
+                    ex,
+                    false
+                )
+            }"
+        )
+        return ResponseEntity("Internal error.", HttpStatus.BAD_REQUEST)
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/I18nRest.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/I18nRest.kt
new file mode 100644
index 0000000..f59f183
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/I18nRest.kt
@@ -0,0 +1,42 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.json.JsonUtils
+import de.micromata.borgbutler.server.I18nClientMessages
+import org.apache.commons.lang3.StringUtils
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+import java.util.*
+import javax.servlet.http.HttpServletRequest
+
+@RestController
+@RequestMapping("/rest/i18n")
+class I18nRest {
+
+    /**
+     *
+     * @param request For detecting the user's client locale.
+     * @param locale If not given, the client's language (browser) will be used.
+     * @param keysOnly If true, only the keys will be returned. Default is false.
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @see JsonUtils.toJson
+     */
+    @GetMapping("list")
+    fun getList(
+        request: HttpServletRequest,
+        @RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean?,
+        @RequestParam("keysOnly", required = false) keysOnly: Boolean?,
+        @RequestParam("locale", required = false) locale: String?
+    ): String {
+        val localeObject: Locale?
+        if (StringUtils.isNotBlank(locale)) {
+            localeObject = Locale(locale)
+        } else {
+            localeObject = RestUtils.getUserLocale(request)
+        }
+        val translations: Map<String, String> =
+            I18nClientMessages.getInstance().getAllMessages(localeObject, keysOnly == true)
+        return JsonUtils.toJson(translations, prettyPrinter)
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/JobsRest.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/JobsRest.kt
new file mode 100644
index 0000000..f614f8b
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/JobsRest.kt
@@ -0,0 +1,201 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.BorgQueueExecutor
+import de.micromata.borgbutler.config.ConfigurationHandler
+import de.micromata.borgbutler.jobs.AbstractJob
+import de.micromata.borgbutler.json.JsonUtils
+import de.micromata.borgbutler.json.borg.ProgressInfo
+import de.micromata.borgbutler.server.rest.queue.JsonJob
+import de.micromata.borgbutler.server.rest.queue.JsonJobQueue
+import mu.KotlinLogging
+import org.apache.commons.collections4.CollectionUtils
+import org.apache.commons.lang3.StringUtils
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+
+private val log = KotlinLogging.logger {}
+
+@RestController
+@RequestMapping("/rest/jobs")
+class JobsRest {
+    /**
+     * @param repo If given, only the job queue of the given repo will be returned.
+     * @param testMode If true, then a test job list is created.
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @return Job queues as json string.
+     * @see JsonUtils.toJson
+     */
+    @GetMapping
+    fun getJobs(
+        @RequestParam("repo", required = false) repo: String?,
+        @RequestParam("testMode", required = false) testMode: Boolean?,
+        @RequestParam("oldJobs", required = false) oldJobs: Boolean?,
+        @RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean?
+    ): String {
+        log.debug("getJobs repo=$repo, oldJobs=$oldJobs")
+        if (testMode == true) {
+            // Return dynamic test queue:
+            return returnTestList(oldJobs, prettyPrinter)
+        }
+        var validRepo = false
+        if (StringUtils.isNotBlank(repo) && "null" != repo && "undefined" != repo) {
+            validRepo = true
+        }
+        val borgQueueExecutor = BorgQueueExecutor.getInstance()
+        val queueList = mutableListOf<JsonJobQueue>()
+        if (validRepo) { // Get only the queue of the given repo:
+            val queue = getQueue(repo, oldJobs)
+            if (queue != null) {
+                queueList.add(queue)
+            }
+        } else { // Get all the queues (of all repos).
+            for (rep in borgQueueExecutor.getRepos()) {
+                val queue = getQueue(rep, oldJobs)
+                if (queue != null) {
+                    queueList.add(queue)
+                }
+            }
+        }
+        return JsonUtils.toJson(queueList, prettyPrinter)
+    }
+
+    private fun getQueue(repo: String?, oldJobs: Boolean?): JsonJobQueue? {
+        val borgQueueExecutor: BorgQueueExecutor = BorgQueueExecutor.getInstance()
+        val repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(repo) ?: return null
+        val borgJobList = borgQueueExecutor.getJobListCopy(repoConfig, oldJobs == true)
+        if (CollectionUtils.isEmpty(borgJobList)) return null
+        val queue: JsonJobQueue = JsonJobQueue().setRepo(repoConfig.getDisplayName())
+        queue.setJobs(mutableListOf())
+        for (borgJob in borgJobList) {
+            val job = JsonJob(borgJob)
+            queue.getJobs().add(job)
+        }
+        return queue
+    }
+
+    /**
+     * @param uniqueJobNumberString The id of the job to cancel.
+     */
+    @GetMapping("/cancel")
+    fun cancelJob(@RequestParam("uniqueJobNumber") uniqueJobNumberString: String) {
+        val uniqueJobNumber =
+            try {
+                uniqueJobNumberString.toLong()
+            } catch (ex: NumberFormatException) {
+                log.error("Can't cancel job, because unique job number couln't be parsed (long value expected): $uniqueJobNumberString")
+                return
+            }
+        BorgQueueExecutor.getInstance().cancelJob(uniqueJobNumber)
+    }
+
+    /**
+     * Only for test purposes and development.
+     *
+     * @param oldJobs
+     * @param prettyPrinter
+     * @return
+     */
+    private fun returnTestList(oldJobs: Boolean?, prettyPrinter: Boolean?): String {
+        var list = if (oldJobs == true) oldJobsTestList else testList
+        if (list == null) {
+            list = mutableListOf()
+            var uniqueJobNumber: Long = 100000
+            var queue: JsonJobQueue = JsonJobQueue().setRepo("My Computer")
+            addTestJob(queue, "info", "my-macbook", 0, 2342, uniqueJobNumber++, oldJobs == true)
+            addTestJob(queue, "list", "my-macbook", -1, -1, uniqueJobNumber++, oldJobs == true)
+            list.add(queue)
+            queue = JsonJobQueue().setRepo("My Server")
+            addTestJob(queue, "list", "my-server", 0, 1135821, uniqueJobNumber++, oldJobs == true)
+            addTestJob(queue, "info", "my-server", -1, -1, uniqueJobNumber++, oldJobs == true)
+            list.add(queue)
+            if (oldJobs == true) {
+                oldJobsTestList = list
+            } else {
+                testList = list
+            }
+        } else if (oldJobs != true) {
+            for (jobQueue in list) {
+                for (job in jobQueue.getJobs()) {
+                    if (job.getStatus() != AbstractJob.Status.RUNNING) continue
+                    var current: Long = job.getProgressInfo().getCurrent()
+                    val total: Long = job.getProgressInfo().getTotal()
+                    current += if (StringUtils.startsWith(job.getProgressInfo().getMessage(), "Calculating")) {
+                        // Info is a faster operation:
+                        (Math.random() * total / 5).toLong()
+                    } else {
+                        // than get the complete archive file list:
+                        (Math.random() * total / 30).toLong()
+                    }
+                    if (current > total) {
+                        current = 0 // Reset to beginning.
+                    }
+                    job.getProgressInfo().setCurrent(current)
+                    if (job.getProgressText().startsWith("Calculating")) {
+                        job.getProgressInfo()
+                            .setMessage("Calculating statistics...  " + Math.round((100 * current / total).toFloat()) + "%")
+                    }
+                    job.buildProgressText()
+                }
+            }
+        }
+        return JsonUtils.toJson(list, prettyPrinter)
+    }
+
+    /**
+     * Only for test purposes and development.
+     *
+     * @param queue
+     * @param operation
+     * @param host
+     * @param current
+     * @param total
+     * @return
+     */
+    private fun addTestJob(
+        queue: JsonJobQueue,
+        operation: String,
+        host: String,
+        current: Long,
+        total: Long,
+        uniqueNumber: Long,
+        oldJobs: Boolean
+    ): JsonJob {
+        val progressInfo = ProgressInfo()
+            .setCurrent(current)
+            .setTotal(total)
+        val job: JsonJob = JsonJob()
+            .setProgressInfo(progressInfo)
+            .setStatus(AbstractJob.Status.QUEUED)
+        if ("info" == operation) {
+            progressInfo.setMessage("Calculating statistics... ")
+            job.setDescription("Loading info of archive '" + host + "-2018-12-05T23:10:33' of repo '" + queue.getRepo() + "'.")
+                .setCommandLineAsString("borg info --json --log-json --progress ssh://...:23/./backups/$host::$host-2018-12-05T23:10:33")
+        } else {
+            progressInfo.setMessage("Getting file list... ")
+            job.setDescription("Loading list of files of archive '" + host + "-2018-12-05T17:30:38' of repo '" + queue.getRepo() + "'.")
+                .setCommandLineAsString("borg list --json-lines ssh://...:23/./backups/$host::$host-2018-12-05T17:30:38")
+        }
+        job.buildProgressText()
+        if (current >= 0) {
+            job.setStatus(AbstractJob.Status.RUNNING)
+        } else {
+            job.setStatus(AbstractJob.Status.QUEUED)
+        }
+        if (queue.getJobs() == null) {
+            queue.setJobs(ArrayList<JsonJob>())
+        }
+        job.setUniqueJobNumber(uniqueNumber)
+        if (oldJobs) {
+            job.setStatus(if (uniqueNumber % 2 == 0L) AbstractJob.Status.CANCELLED else AbstractJob.Status.DONE)
+        }
+        queue.getJobs().add(job)
+        return job
+    }
+
+    companion object {
+        private var testList: MutableList<JsonJobQueue>? = null
+        private var oldJobsTestList: MutableList<JsonJobQueue>? = null
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/LoggingRest.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/LoggingRest.kt
new file mode 100644
index 0000000..24036a3
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/LoggingRest.kt
@@ -0,0 +1,67 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.json.JsonUtils
+import de.micromata.borgbutler.server.logging.LoggerMemoryAppender
+import de.micromata.borgbutler.server.logging.LogFilter
+import de.micromata.borgbutler.server.logging.LogLevel
+import mu.KotlinLogging
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+import javax.servlet.http.HttpServletRequest
+
+private val log = KotlinLogging.logger {}
+
+@RestController
+@RequestMapping("/rest/logging")
+class LoggingRest {
+    /**
+     * @param request
+     * @param search
+     * @param logLevelTreshold fatal, error, warn, info, debug or trace (case insensitive).
+     * @param maxSize          Max size of the result list.
+     * @param ascendingOrder   Default is false (default is descending order).
+     * @param lastReceivedOrderNumber The last received order number for updating log entries (preventing querying all entries again).
+     * @param prettyPrinter
+     * @return
+     */
+    @GetMapping("query")
+    fun query(
+        request: HttpServletRequest?,
+        @RequestParam("search", required = false) search: String?,
+        @RequestParam("treshold", required = false) logLevelTreshold: String?,
+        @RequestParam("maxSize", required = false) maxSize: Int?,
+        @RequestParam("ascendingOrder", required = false) ascendingOrder: Boolean?,
+        @RequestParam("lastReceivedOrderNumber", required = false) lastReceivedOrderNumber: Int?,
+        @RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean?
+    ): String {
+        val filter = LogFilter()
+        filter.setSearch(search)
+        if (logLevelTreshold != null) {
+            try {
+                val treshold =
+                    LogLevel.valueOf(logLevelTreshold.trim { it <= ' ' }
+                        .toUpperCase())
+                filter.setThreshold(treshold)
+            } catch (ex: IllegalArgumentException) {
+                log.error("Can't parse log level treshold: " + logLevelTreshold + ". Supported values (case insensitive): " + LogLevel.getSupportedValues())
+            }
+        }
+        if (filter.getThreshold() == null) {
+            filter.setThreshold(LogLevel.INFO)
+        }
+        if (maxSize != null) {
+            filter.setMaxSize(maxSize)
+        }
+        if (ascendingOrder != null && ascendingOrder == true) {
+            filter.setAscendingOrder(true)
+        }
+        if (lastReceivedOrderNumber != null) {
+            filter.setLastReceivedLogOrderNumber(lastReceivedOrderNumber)
+        }
+        val loggerMemoryAppender = LoggerMemoryAppender.getInstance()
+        return JsonUtils.toJson(loggerMemoryAppender.query(filter, RestUtils.getUserLocale(request!!)), prettyPrinter)
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ReposRest.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ReposRest.kt
new file mode 100644
index 0000000..eed277a
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/ReposRest.kt
@@ -0,0 +1,66 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.cache.ButlerCache
+import de.micromata.borgbutler.data.Repository
+import de.micromata.borgbutler.json.JsonUtils
+import mu.KotlinLogging
+import org.apache.commons.collections4.CollectionUtils
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+
+private val log = KotlinLogging.logger {}
+
+@RestController
+@RequestMapping("/rest/repos")
+class ReposRest {
+    /**
+     *
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @return A list of repositories of type [BorgRepository].
+     * @see JsonUtils.toJson
+     */
+    @GetMapping("list")
+    fun getList(@RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean?): String {
+        val repositories: List<Repository?> = ButlerCache.getInstance().getAllRepositories()
+        return if (CollectionUtils.isEmpty(repositories)) {
+            "[]"
+        } else JsonUtils.toJson(repositories, prettyPrinter)
+    }
+
+    /**
+     *
+     * @param id id or name of repo.
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @return [Repository] (without list of archives) as json string.
+     * @see JsonUtils.toJson
+     */
+    @GetMapping("repo")
+    fun getRepo(@RequestParam("id") id: String,
+                @RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean?): String {
+        val repository: Repository = ButlerCache.getInstance().getRepository(id)
+        return JsonUtils.toJson(repository, prettyPrinter)
+    }
+
+    /**
+     *
+     * @param id id or name of repo.
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @return [Repository] (including list of archives) as json string.
+     * @see JsonUtils.toJson
+     */
+    @GetMapping("repoArchiveList")
+    fun getRepoArchiveList(
+        @RequestParam("id") id: String,
+        @RequestParam("force", required = false) force: Boolean?,
+        @RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean?
+    ): String {
+        if (force == true) {
+            val repo: Repository = ButlerCache.getInstance().getRepository(id)
+            ButlerCache.getInstance().clearRepoCacheAccess(repo)
+        }
+        val repository: Repository = ButlerCache.getInstance().getRepositoryArchives(id)
+        return JsonUtils.toJson(repository, prettyPrinter)
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/RequestLog.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/RequestLog.kt
new file mode 100644
index 0000000..4a2bb7f
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/RequestLog.kt
@@ -0,0 +1,138 @@
+/////////////////////////////////////////////////////////////////////////////
+//
+// Project ProjectForge Community Edition
+//         www.projectforge.org
+//
+// Copyright (C) 2001-2021 Micromata GmbH, Germany (www.micromata.com)
+//
+// ProjectForge is dual-licensed.
+//
+// This community edition is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as published
+// by the Free Software Foundation; version 3 of the License.
+//
+// This community edition 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 General
+// Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see http://www.gnu.org/licenses/.
+//
+/////////////////////////////////////////////////////////////////////////////
+
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.json.JsonUtils
+import java.security.Principal
+import java.util.*
+import javax.servlet.http.Cookie
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.Part
+
+/**
+ * Helper class for debugging requests. Converts a given request to json.
+ */
+object RequestLog {
+    @JvmStatic
+    fun asJson(request: HttpServletRequest, longForm: Boolean = false): String {
+        val data = RequestData(request, longForm)
+        return JsonUtils.toJson(data)
+    }
+
+    @JvmStatic
+    fun asString(request: HttpServletRequest): String {
+        return request.requestURI
+    }
+}
+
+class RequestData(request: HttpServletRequest, longForm: Boolean = false) {
+    val attributes = mutableMapOf<String, Any?>()
+    val parameters = mutableMapOf<String, String?>()
+    val headers = mutableMapOf<String, String?>()
+    var locales: MutableList<Locale>? = null
+    //val parts = mutableListOf<PartInfo>()
+
+    val authType: String? = request.authType
+    val characterEncoding: String? = request.characterEncoding
+    val contentLength: Int? = if (longForm) request.contentLength else null
+    val contentType: String? = if (longForm) request.contentType else null
+    val contextPath: String? = if (longForm) request.contextPath else null
+    val cookies: Array<Cookie>? = if (longForm) request.cookies else null
+    val isAsyncStarted: Boolean? = if (longForm) request.isAsyncStarted else null
+    val isRequestedSessionIdFromCookie: Boolean? = if (longForm) request.isRequestedSessionIdFromCookie else null
+    val isRequestedSessionIdFromURL: Boolean? = if (longForm) request.isRequestedSessionIdFromURL else null
+    val isRequestedSessionIdValid: Boolean? = if (longForm) request.isRequestedSessionIdValid else null
+    val isSecure: Boolean? = if (longForm) request.isSecure else null
+    val isTrailerFieldsReady: Boolean? = if (longForm) request.isTrailerFieldsReady else null
+    val localAddr: String? = if (longForm) request.localAddr else null
+    val localName: String? = if (longForm) request.localName else null
+    val localPort: Int? = if (longForm) request.localPort else null
+    val locale: Locale? = if (longForm) request.locale else null
+    val method: String? = request.method
+    val pathInfo: String? = if (longForm) request.pathInfo else null
+    val pathTranslated: String? = if (longForm) request.pathTranslated else null
+    val protocol: String? = if (longForm) request.protocol else null
+    val queryString: String? = request.queryString
+    val remoteAddr: String? = request.remoteAddr
+    val remoteHost: String? = if (longForm) request.remoteHost else null
+    val remotePort: Int? = if (longForm) request.remotePort else null
+    val remoteUser: String? = request.remoteUser
+    val requestedSessionId: String? = if (longForm) request.requestedSessionId else null
+    val requestUri: String? = request.requestURI
+    val scheme: String? = if (longForm) request.scheme else null
+    val serverName = if (longForm) request.serverName else null
+    val serverPort: Int? = if (longForm) request.serverPort else null
+    val servletPath: String? = if (longForm) request.servletPath else null
+    val sessionId: String? = request.session?.id
+    val userPrincipal: Principal? = request.userPrincipal
+
+
+    init {
+        for (attribute in request.attributeNames) {
+            attributes[attribute] = handleSecret(request, attribute, request.getAttribute(attribute)?.toString())
+        }
+        for (header in request.headerNames) {
+            headers[header] = handleSecret(request, header, request.getHeader(header)) as String
+        }
+        if (longForm) {
+            locales = mutableListOf()
+            locales?.let {
+                for (locale in request.locales) {
+                    it.add(locale)
+                }
+            }
+        }
+        for (parameter in request.parameterNames) {
+            parameters[parameter] = handleSecret(request, parameter, request.getParameter(parameter)) as String
+        }
+        /*
+        for (part in request.parts) {
+            parts.add(PartInfo(part))
+        }*/
+    }
+
+    private fun <T> handleSecret(request: HttpServletRequest, name: String?, value: T?): Any? {
+        name ?: return null
+        value ?: return null
+        if (name.toLowerCase() == "authorization") {
+            return "authorization=******"
+        }
+        return value
+    }
+}
+
+class PartInfo(part: Part) {
+    val headers = mutableMapOf<String, String>()
+    // Can't get inputstream
+
+    val contentType: String? = part.contentType
+    val name: String? = part.name
+    val size = part.size
+
+    init {
+        for (header in part.headerNames) {
+            headers[header] = part.getHeader(header)
+        }
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/RestUtils.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/RestUtils.kt
new file mode 100644
index 0000000..9586f7d
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/RestUtils.kt
@@ -0,0 +1,112 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.server.RunningMode
+import de.micromata.borgbutler.server.user.UserData
+import de.micromata.borgbutler.server.user.UserUtils
+import org.slf4j.Logger
+import org.springframework.core.io.ByteArrayResource
+import org.springframework.core.io.InputStreamResource
+import org.springframework.core.io.Resource
+import org.springframework.http.HttpHeaders
+import org.springframework.http.MediaType
+import org.springframework.http.ResponseEntity
+import java.io.InputStream
+import java.net.InetAddress
+import java.net.UnknownHostException
+import java.util.*
+import javax.servlet.ServletRequest
+import javax.servlet.http.HttpServletRequest
+
+object RestUtils {
+    /**
+     * @return null, if the local app (JavaFX) is running and the request is from localhost. Otherwise message, why local
+     * service isn't available.
+     */
+    fun checkLocalDesktopAvailable(requestContext: HttpServletRequest): String? {
+        if (RunningMode.getServerType() != RunningMode.ServerType.DESKTOP) {
+            return "Service unavailable. No desktop app on localhost available."
+        }
+        val remoteAddr = requestContext.remoteAddr
+        return if (remoteAddr == null || remoteAddr != "127.0.0.1") {
+            "Service not available. Can't call this service remote. Run this service on localhost of the running desktop app."
+        } else null
+    }
+
+    /**
+     * @return Returns the user put by the UserFilter.
+     * @see UserUtils.getUser
+     * @see de.micromata.borgbutler.server.user.UserFilter
+     */
+    fun getUser(): UserData {
+        val user = UserUtils.getUser() ?: throw IllegalStateException("No user given in rest call.")
+        return UserUtils.getUser()
+    }
+
+    fun getUserLocale(requestContext: HttpServletRequest): Locale? {
+        val user = getUser()
+        var locale = user.locale
+        if (locale == null) {
+            locale = requestContext.locale
+        }
+        return locale
+    }
+
+    @JvmStatic
+    fun getClientIp(request: ServletRequest): String? {
+        var remoteAddr: String? = null
+        if (request is HttpServletRequest) {
+            remoteAddr = request.getHeader("X-Forwarded-For")
+        }
+        if (remoteAddr != null) {
+            if (remoteAddr.contains(",")) {
+                // sometimes the header is of form client ip,proxy 1 ip,proxy 2 ip,...,proxy n ip,
+                // we just want the client
+                remoteAddr = remoteAddr.split(',')[0].trim({ it <= ' ' })
+            }
+            try {
+                // If ip4/6 address string handed over, simply does pattern validation.
+                InetAddress.getByName(remoteAddr)
+            } catch (e: UnknownHostException) {
+                remoteAddr = request.remoteAddr
+            }
+
+        } else {
+            remoteAddr = request.remoteAddr
+        }
+        return remoteAddr
+    }
+
+    fun downloadFile(filename: String, inputStream: InputStream): ResponseEntity<InputStreamResource> {
+        return ResponseEntity.ok()
+            .contentType(MediaType.parseMediaType("application/octet-stream"))
+            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"${filename.replace('"', '_')}\"")
+            .body(InputStreamResource(inputStream))
+    }
+
+    fun downloadFile(filename: String, content: String): ResponseEntity<String> {
+        return ResponseEntity.ok()
+            .contentType(MediaType.parseMediaType("application/octet-stream"))
+            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"${filename.replace('"', '_')}\"")
+            .body(content)
+    }
+
+    fun downloadFile(filename: String, resource: ByteArrayResource): ResponseEntity<Resource> {
+        return ResponseEntity.ok()
+            .contentType(MediaType.parseMediaType("application/octet-stream"))
+            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"${filename.replace('"', '_')}\"")
+            .body(resource)
+    }
+
+    fun badRequest(message: String): ResponseEntity<String> {
+        return ResponseEntity.badRequest().body(message)
+    }
+
+    fun notFound(): ResponseEntity<String> {
+        return ResponseEntity.badRequest().body("Not found.")
+    }
+
+    fun notFound(log: Logger, errorMessage: String?): ResponseEntity<String> {
+        log.error(errorMessage)
+        return ResponseEntity.badRequest().body(errorMessage)
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/SystemInfo.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/SystemInfo.kt
new file mode 100644
index 0000000..d6a9d15
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/SystemInfo.kt
@@ -0,0 +1,33 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.BorgQueueStatistics
+import de.micromata.borgbutler.server.BorgVersion
+
+/**
+ * Statistics of all the job queues, especially the number of total queued and running jobs.
+ * This is used e. g. by the client for showing a badge near to the menu entry "job monitor" with the number
+ * of Jobs in the queues.
+ */
+class SystemInfo {
+    var queueStatistics: BorgQueueStatistics? = null
+        private set
+    var isConfigurationOK = false
+        private set
+    var borgVersion: BorgVersion? = null
+        private set
+
+    fun setQueueStatistics(queueStatistics: BorgQueueStatistics?): SystemInfo {
+        this.queueStatistics = queueStatistics
+        return this
+    }
+
+    fun setConfigurationOK(configurationOK: Boolean): SystemInfo {
+        isConfigurationOK = configurationOK
+        return this
+    }
+
+    fun setBorgVersion(borgVersion: BorgVersion?): SystemInfo {
+        this.borgVersion = borgVersion
+        return this
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/SystemInfoRest.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/SystemInfoRest.kt
new file mode 100644
index 0000000..943cbb4
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/SystemInfoRest.kt
@@ -0,0 +1,26 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.BorgQueueExecutor
+import de.micromata.borgbutler.json.JsonUtils
+import de.micromata.borgbutler.server.BorgInstallation
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/rest/system")
+class SystemInfoRest {
+    /**
+     * @return The total number of jobs queued or running (and other statistics): [de.micromata.borgbutler.BorgQueueStatistics].
+     * @see JsonUtils.toJson
+     */
+    @GetMapping("info")
+    fun statistics(): SystemInfo {
+        val borgVersion = BorgInstallation.getInstance().getBorgVersion()
+        val systemInfonfo = SystemInfo()
+            .setQueueStatistics(BorgQueueExecutor.getInstance().getStatistics())
+            .setConfigurationOK(borgVersion.isVersionOK())
+            .setBorgVersion(borgVersion)
+        return systemInfonfo
+    }
+}
diff --git a/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/VersionRest.kt b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/VersionRest.kt
new file mode 100644
index 0000000..dfa3165
--- /dev/null
+++ b/borgbutler-server/src/main/kotlin/de/micromata/borgbutler/server/rest/VersionRest.kt
@@ -0,0 +1,66 @@
+package de.micromata.borgbutler.server.rest
+
+import de.micromata.borgbutler.json.JsonUtils
+import de.micromata.borgbutler.server.Languages
+import de.micromata.borgbutler.server.Version
+import org.apache.commons.lang3.StringUtils
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+import java.util.*
+import javax.servlet.http.HttpServletRequest
+
+@RestController
+@RequestMapping("/rest")
+class VersionRest {
+    /**
+     *
+     * @param request For detecting the user's client locale.
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @see JsonUtils.toJson
+     */
+    @GetMapping("version")
+    fun getVersion(
+        request: HttpServletRequest,
+        @RequestParam("prettyPrinter", required = false) prettyPrinter: Boolean?
+    ): String {
+        val user = RestUtils.getUser()
+        var language = Languages.asString(user.getLocale())
+        if (StringUtils.isBlank(language)) {
+            val locale: Locale = request.locale
+            language = locale.getLanguage()
+        }
+        val version = MyVersion(language, RestUtils.checkLocalDesktopAvailable(request) == null)
+        return JsonUtils.toJson(version, prettyPrinter)
+    }
+
+    inner class MyVersion(language: String, localDesktopAvailable: Boolean) {
+        private val version: Version
+        val language: String
+        val isLocalDesktopAvailable: Boolean
+        val appName: String
+            get() = version.appName
+
+        fun getVersion(): String {
+            return version.version
+        }
+
+        val buildDateUTC: String
+            get() = version.buildDateUTC
+        val buildDate: Date
+            get() = version.buildDate
+
+        /**
+         * @return Version of the available update, if exist. Otherwise null.
+         */
+        val updateVersion: String?
+            get() = version.updateVersion
+
+        init {
+            version = Version.getInstance()
+            this.language = language
+            isLocalDesktopAvailable = localDesktopAvailable
+        }
+    }
+}
diff --git a/borgbutler-server/src/main/resources/application.properties b/borgbutler-server/src/main/resources/application.properties
new file mode 100644
index 0000000..0388a89
--- /dev/null
+++ b/borgbutler-server/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+server.address=127.0.0.1
+server.port=9042
diff --git a/borgbutler-server/src/main/resources/log4j.properties b/borgbutler-server/src/main/resources/log4j.properties
deleted file mode 100644
index ee7031b..0000000
--- a/borgbutler-server/src/main/resources/log4j.properties
+++ /dev/null
@@ -1,21 +0,0 @@
-log4j.rootLogger=info, stdout, memory, file
-#log4j.logger.de.micromata.borgbutler.persistency=debug
-#log4j.logger.de.micromata.borgbutler.main.jetty=debug
-
-log4j.logger.org.apache.commons.jcs=WARN
-
-log4j.appender.stdout=org.apache.log4j.ConsoleAppender
-log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
-
-# Pattern to output the caller's file name and line number.
-log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
-
-log4j.appender.memory=de.micromata.borgbutler.server.logging.Log4jMemoryAppender
-
-log4j.appender.file=org.apache.log4j.RollingFileAppender
-
-log4j.appender.file.File=${borgbutlerHome}borgbutler.log
-log4j.appender.file.MaxFileSize=10MB
-log4j.appender.file.MaxBackupIndex=5
-log4j.appender.file.layout=org.apache.log4j.PatternLayout
-log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
diff --git a/borgbutler-server/src/main/resources/logback-spring.xml b/borgbutler-server/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..ddf1bbf
--- /dev/null
+++ b/borgbutler-server/src/main/resources/logback-spring.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
+    <property name="LOG_HOME" value="${borgbutlerHome:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}}"/>
+    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
+
+    <appender name="ROLLING-FILE-ALL"
+              class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <encoder>
+            <pattern>%d{MM-dd-yy HH:mm:ss} %-5level %mdc %logger{60}::%M:%line -
+                %msg%n
+            </pattern>
+        </encoder>
+        <file>${LOG_HOME}borgbutler.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/borgbutler.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <minIndex>1</minIndex>
+            <maxIndex>10</maxIndex>
+        </rollingPolicy>
+        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
+            <maxFileSize>10MB</maxFileSize>
+        </triggeringPolicy>
+    </appender>
+
+    <appender name="ROLLING-MEMORY" class="de.micromata.borgbutler.server.logging.LoggerMemoryAppender" />
+
+    <root level="INFO">
+        <appender-ref ref="CONSOLE"/>
+        <appender-ref ref="ROLLING-FILE-ALL"/>
+        <appender-ref ref="ROLLING-MEMORY"/>
+    </root>
+
+    <!-- custom logging levels -->
+    <!--logger name="de.micromata.merlin.excel" level="DEBUG" /-->
+</configuration>
diff --git a/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx b/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx
index cb960cc..97334b0 100644
--- a/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx
+++ b/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx
@@ -25,7 +25,7 @@
         fetch(getRestServiceUrl('archives', {
             repo: this.state.repoId,
             archiveId: this.state.archiveId,
-            force: force
+            force: force === true
         }), {
             method: 'GET',
             headers: {
diff --git a/borgbutler-webapp/src/components/views/archives/FileListPanel.jsx b/borgbutler-webapp/src/components/views/archives/FileListPanel.jsx
index b698f1a..5dd4a63 100644
--- a/borgbutler-webapp/src/components/views/archives/FileListPanel.jsx
+++ b/borgbutler-webapp/src/components/views/archives/FileListPanel.jsx
@@ -137,7 +137,7 @@
         fetch(getRestServiceUrl('archives/filelist', {
             archiveId: this.props.archive.id,
             diffArchiveId: this.state.filter.diffArchiveId,
-            force: force,
+            force: force === true,
             searchString: this.state.filter.search,
             mode: this.state.filter.mode,
             currentDirectory: this.state.filter.currentDirectory,
diff --git a/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx b/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx
index 6cc440b..1b5636c 100644
--- a/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx
+++ b/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx
@@ -31,7 +31,7 @@
         });
         fetch(getRestServiceUrl('repos/repoArchiveList', {
             id: this.state.id,
-            force: force
+            force: force === true,
         }), {
             method: 'GET',
             headers: {
diff --git a/build.gradle b/build.gradle
index 96b1dc8..5141f59 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,15 +6,20 @@
  * user guide available at https://docs.gradle.org/5.0/userguide/tutorial_java_projects.html
  */
 
+plugins {
+    id "org.jetbrains.kotlin.jvm" version "1.4.32" apply false
+}
+
 allprojects {
     apply plugin: 'maven'
 
     group = 'de.micromata.borgbutler'
-    version = '0.4-SNAPSHOT'
+    version = '0.5-SNAPSHOT'
 }
 
 subprojects {
     apply plugin: 'java'
+    apply plugin: 'org.jetbrains.kotlin.jvm'
     sourceCompatibility = 1.9 // Needed: since 1.9 i18n properties in UTF-8 format.
     targetCompatibility = 1.9
 
@@ -31,8 +36,6 @@
     }
 
     dependencies {
-        compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
-        testCompile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25'
         testImplementation(
                 'org.junit.jupiter:junit-jupiter-api:5.3.0'
         )
@@ -42,10 +45,6 @@
         )
     }
 
-    subprojects {
-        ext.depVersions = ['commonsio': '2.8.0', dep2: '2.0']
-    }
-
     test {
         useJUnitPlatform()
     }
diff --git a/doc/Development.adoc b/doc/Development.adoc
index c819664..3e2de5f 100644
--- a/doc/Development.adoc
+++ b/doc/Development.adoc
@@ -28,7 +28,7 @@
 1. `cd borgbutler-webapp`
 2. `npm install`
 3. `gradle npmBuild` (builds the web archive)
-4. Start `de.micromata.borgbutler.server.Main`
+4. Start `BorgButlerApplication`
 
 === Start borgbutler-server for web development
 For using hot code replacement of your web files, you should use `npm start` or `yarn start`:
@@ -36,7 +36,7 @@
 1. `cd borgbutler-webapp`
 2. `npm install`
 3. `npm start` (opens the web browser on port 3000)
-4. Start `de.micromata.borgbutler.server.Main` (ignore the opened browser window for port 9042)
+4. Start `BorgButlerApplication` (ignore the opened browser window for port 9042)
 
 === Profiling heap, cpu and everything using JProfiler
 JProfiler is an excellent tool for analysing your software. BorgButler was optimized regarding heap memory and CPU usage by

--
Gitblit v1.10.0