diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml
index 2d4e47b08..a1c57d91c 100644
--- a/.github/workflows/appimage.yml
+++ b/.github/workflows/appimage.yml
@@ -122,6 +122,7 @@ jobs:
--java-options "-Dcryptomator.appVersion=\"${{ needs.get-version.outputs.semVerStr }}\""
--java-options "-Dfile.encoding=\"utf-8\""
--java-options "-Djava.net.useSystemProxies=true"
+ --java-options "-Dcryptomator.adminConfigPath=\"/etc/cryptomator/config.properties\""
--java-options "-Dcryptomator.logDir=\"@{userhome}/.local/share/Cryptomator/logs\""
--java-options "-Dcryptomator.pluginDir=\"@{userhome}/.local/share/Cryptomator/plugins\""
--java-options "-Dcryptomator.settingsPath=\"@{userhome}/.config/Cryptomator/settings.json:@{userhome}/.Cryptomator/settings.json\""
diff --git a/.github/workflows/mac-dmg-x64.yml b/.github/workflows/mac-dmg-x64.yml
index b1677458e..d8047462e 100644
--- a/.github/workflows/mac-dmg-x64.yml
+++ b/.github/workflows/mac-dmg-x64.yml
@@ -128,6 +128,7 @@ jobs:
--java-options "-Dapple.awt.enableTemplateImages=true"
--java-options "-Dsun.java2d.metal=true"
--java-options "-Dcryptomator.appVersion=\"${{ needs.get-version.outputs.semVerStr }}\""
+ --java-options "-Dcryptomator.adminConfigPath=\"/Library/Application Support/Cryptomator/config.properties\""
--java-options "-Dcryptomator.logDir=\"@{userhome}/Library/Logs/Cryptomator\""
--java-options "-Dcryptomator.pluginDir=\"@{userhome}/Library/Application Support/Cryptomator/Plugins\""
--java-options "-Dcryptomator.settingsPath=\"@{userhome}/Library/Application Support/Cryptomator/settings.json\""
diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml
index 8b341b56a..9a06024a0 100644
--- a/.github/workflows/mac-dmg.yml
+++ b/.github/workflows/mac-dmg.yml
@@ -126,6 +126,7 @@ jobs:
--java-options "-Dapple.awt.enableTemplateImages=true"
--java-options "-Dsun.java2d.metal=true"
--java-options "-Dcryptomator.appVersion=\"${{ needs.get-version.outputs.semVerStr }}\""
+ --java-options "-Dcryptomator.adminConfigPath=\"/Library/Application Support/Cryptomator/config.properties\""
--java-options "-Dcryptomator.logDir=\"@{userhome}/Library/Logs/Cryptomator\""
--java-options "-Dcryptomator.pluginDir=\"@{userhome}/Library/Application Support/Cryptomator/Plugins\""
--java-options "-Dcryptomator.settingsPath=\"@{userhome}/Library/Application Support/Cryptomator/settings.json\""
diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml
index 749cab0c2..23846ad5d 100644
--- a/.github/workflows/win-exe.yml
+++ b/.github/workflows/win-exe.yml
@@ -140,6 +140,7 @@ jobs:
--java-options "-Dcryptomator.appVersion=\"${{ needs.get-version.outputs.semVerStr }}\""
--java-options "-Dfile.encoding=\"utf-8\""
--java-options "-Djava.net.useSystemProxies=true"
+ --java-options "-Dcryptomator.adminConfigPath=\"C:/ProgramData/Cryptomator/config.properties\""
--java-options "-Dcryptomator.logDir=\"@{localappdata}/Cryptomator\""
--java-options "-Dcryptomator.pluginDir=\"@{appdata}/Cryptomator/Plugins\""
--java-options "-Dcryptomator.settingsPath=\"@{appdata}/Cryptomator/settings.json;@{userhome}/AppData/Roaming/Cryptomator/settings.json\""
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10f1135e8..6a071b2fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ Changes to prior versions can be found on the [Github release page](https://gith
* Mark files in-use for Hub vaults ([#4078](https://github.com/cryptomator/cryptomator/pull/4078))
* Accessibility: Adjust app to be used with a screen reader ([#547](https://github.com/cryptomator/cryptomator/issues/547))
* Show Archived Vault Dialog on unlock when Hub returns 410 ([#4081](https://github.com/cryptomator/cryptomator/pull/4081))
+* Admin configuration: Allow overwriting certain app properties by external config file ([#4105](https://github.com/cryptomator/cryptomator/pull/4105))
### Changed
* Built using JDK 25 ([#4031](https://github.com/cryptomator/cryptomator/issues/4031))
diff --git a/dist/common/cryptomator.config b/dist/common/cryptomator.config
new file mode 100644
index 000000000..98c5f753f
--- /dev/null
+++ b/dist/common/cryptomator.config
@@ -0,0 +1,8 @@
+# This is the Cryptomator administrative configuration file.
+# It is a simple key-value pair file.
+# Lines starting with '#' are comments and will be ignored.
+# For more info, read the docs at https://docs.cryptomator.org/desktop/advanced-settings/
+#
+# Example:
+# Sets the plugin directory and enables plugin loading
+# cryptomator.pluginDir=@{userhome}/Cryptomator/Plugins
\ No newline at end of file
diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh
index 99249cb35..ee72b538b 100755
--- a/dist/linux/appimage/build.sh
+++ b/dist/linux/appimage/build.sh
@@ -88,6 +88,7 @@ ${JAVA_HOME}/bin/jpackage \
--app-version "${VERSION}.${REVISION_NO}" \
--java-options "-Dfile.encoding=\"utf-8\"" \
--java-options "-Djava.net.useSystemProxies=true" \
+ --java-options "-Dcryptomator.adminConfigPath=\"/etc/cryptomator/config.properties\"" \
--java-options "-Dcryptomator.logDir=\"@{userhome}/.local/share/Cryptomator/logs\"" \
--java-options "-Dcryptomator.pluginDir=\"@{userhome}/.local/share/Cryptomator/plugins\"" \
--java-options "-Dcryptomator.settingsPath=\"@{userhome}/.config/Cryptomator/settings.json:@{userhome}/.Cryptomator/settings.json\"" \
diff --git a/dist/linux/debian/rules b/dist/linux/debian/rules
index 456a97e89..0d2242f40 100755
--- a/dist/linux/debian/rules
+++ b/dist/linux/debian/rules
@@ -51,6 +51,7 @@ override_dh_auto_build:
--java-options "-Xmx256m" \
--java-options "-Dfile.encoding=\"utf-8\"" \
--java-options "-Djava.net.useSystemProxies=true" \
+ --java-options "-Dcryptomator.adminConfigPath=\"/etc/cryptomator/config.properties\"" \
--java-options "-Dcryptomator.logDir=\"@{userhome}/.local/share/Cryptomator/logs\"" \
--java-options "-Dcryptomator.pluginDir=\"@{userhome}/.local/share/Cryptomator/plugins\"" \
--java-options "-Dcryptomator.settingsPath=\"@{userhome}/.config/Cryptomator/settings.json:@{userhome}/.Cryptomator/settings.json\"" \
diff --git a/dist/mac/dmg/build.sh b/dist/mac/dmg/build.sh
index fbe81931c..438de7b9a 100755
--- a/dist/mac/dmg/build.sh
+++ b/dist/mac/dmg/build.sh
@@ -114,6 +114,7 @@ ${JAVA_HOME}/bin/jpackage \
--java-options "-Dapple.awt.enableTemplateImages=true" \
--java-options "-Dsun.java2d.metal=true" \
--java-options "-Dcryptomator.appVersion=\"${VERSION_NO}\"" \
+ --java-options "-Dcryptomator.adminConfigPath=\"/Library/Application Support/Cryptomator/config.properties\"" \
--java-options "-Dcryptomator.logDir=\"@{userhome}/Library/Logs/${APP_NAME}\"" \
--java-options "-XX:ErrorFile=/cryptomator/cryptomator_crash.log" \
--java-options "-Dcryptomator.pluginDir=\"@{userhome}/Library/Application Support/${APP_NAME}/Plugins\"" \
diff --git a/dist/win/build.ps1 b/dist/win/build.ps1
index 6fdee1c9a..54ee8946e 100644
--- a/dist/win/build.ps1
+++ b/dist/win/build.ps1
@@ -155,6 +155,7 @@ $javaOptions = @(
"--java-options", "-Djava.net.useSystemProxies=true"
"--java-options", "-Dcryptomator.logDir=`"@{localappdata}/$AppName`""
"--java-options", "-XX:ErrorFile=`"C:/cryptomator/cryptomator_crash.log`""
+"--java-options", "-Dcryptomator.adminConfigPath=`"C:/ProgramData/$AppName/config.properties`""
"--java-options", "-Dcryptomator.pluginDir=`"@{appdata}/$AppName/Plugins`""
"--java-options", "-Dcryptomator.settingsPath=`"@{appdata}/$AppName/settings.json;@{userhome}/AppData/Roaming/$AppName/settings.json`""
"--java-options", "-Dcryptomator.ipcSocketPath=`"@{localappdata}/$AppName/ipc.socket`""
diff --git a/dist/win/resources/main.wxs b/dist/win/resources/main.wxs
index 7269c29f5..ac5e6a3d1 100644
--- a/dist/win/resources/main.wxs
+++ b/dist/win/resources/main.wxs
@@ -87,7 +87,7 @@
+ * Admin properties are {@link Properties} using system properties as defaults, but allow overwriting a specific set of properties with an external config file. + * Those properties are created by calling {@link #create()}. The method first reads system property {@value #ADMIN_PROP_FILE_KEY}. If it contains a path to a valid properties file, all overridable properties from the file are loaded into the returned admin properties. + *
+ * The overridable properties are: + *
+ * The returned properties object uses as default the {@link System} properties.
+ * For a list of overridable properties, see {@link AdminPropertiesFactory}
+ *
+ * @return {@link Properties} containing overridable properties from the admin config and defaulting to system properties.
+ */
+ static Properties create() {
+ var systemProps = System.getProperties();
+ var adminProps = new Properties(systemProps);
+
+ final String adminCfgPath = System.getProperty(ADMIN_PROP_FILE_KEY);
+ if (adminCfgPath == null) {
+ LOG.debug("Admin config property is not defined. Skipping.");
+ return adminProps;
+ }
+ var propsFromFile = loadPropertiesFromFile(Path.of(adminCfgPath));
+
+ for (var key : propsFromFile.stringPropertyNames()) {
+ if (ALLOWED_OVERRIDES.contains(key)) {
+ var value = propsFromFile.getProperty(key);
+ LOG.info("Overwriting {} with value {} from admin config.", key, value);
+ adminProps.setProperty(key, value);
+ } else {
+ LOG.debug("Property {} in admin config is not supported for override.", key);
+ }
+ }
+ return adminProps;
+ }
+
+ //visible for testing
+ static Properties loadPropertiesFromFile(Path adminPropertiesPath) {
+ var adminProps = new Properties();
+ try (FileChannel ch = FileChannel.open(adminPropertiesPath, StandardOpenOption.READ); //
+ Reader reader = Channels.newReader(ch, StandardCharsets.UTF_8)) {
+ if (ch.size() > MAX_CONFIG_SIZE_BYTES) {
+ throw new IOException("Config file %s exceeds maximum size of %d".formatted(adminPropertiesPath, MAX_CONFIG_SIZE_BYTES));
+ }
+ adminProps.load(reader);
+ } catch (NoSuchFileException _) {
+ //NO-OP
+ LOG.debug("No admin properties found at {}.", adminPropertiesPath);
+ } catch (IOException | IllegalArgumentException e) {
+ LOG.warn("Failed to read administrative properties from {}. Returning empty properties.", adminPropertiesPath, e);
+ }
+ return adminProps;
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/launcher/Cryptomator.java b/src/main/java/org/cryptomator/launcher/Cryptomator.java
index 3e0a613ca..4a6d0750d 100644
--- a/src/main/java/org/cryptomator/launcher/Cryptomator.java
+++ b/src/main/java/org/cryptomator/launcher/Cryptomator.java
@@ -11,9 +11,9 @@ import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.SubstitutingProperties;
-import org.cryptomator.networking.SSLContextProvider;
import org.cryptomator.ipc.IpcCommunicator;
import org.cryptomator.logging.DebugMode;
+import org.cryptomator.networking.SSLContextProvider;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,7 +35,8 @@ public class Cryptomator {
private static final long STARTUP_TIME = System.currentTimeMillis();
static {
- var lazyProcessedProps = new SubstitutingProperties(System.getProperties(), System.getenv());
+ var adminProps = AdminPropertiesFactory.create();
+ var lazyProcessedProps = new SubstitutingProperties(adminProps, System.getenv());
System.setProperties(lazyProcessedProps);
CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.factory().create(STARTUP_TIME);
LOG = LoggerFactory.getLogger(Cryptomator.class);
@@ -64,6 +65,7 @@ public class Cryptomator {
}
public static void main(String[] args) {
+ EventualLogger.INSTANCE.drainTo(LOG);
var printVersion = Optional.ofNullable(args) //
.stream() //Streams either one element (the args-array) or zero elements
.flatMap(Arrays::stream) //
diff --git a/src/main/java/org/cryptomator/launcher/EventualLogger.java b/src/main/java/org/cryptomator/launcher/EventualLogger.java
new file mode 100644
index 000000000..fac429cfe
--- /dev/null
+++ b/src/main/java/org/cryptomator/launcher/EventualLogger.java
@@ -0,0 +1,106 @@
+package org.cryptomator.launcher;
+
+import org.slf4j.Logger;
+import org.slf4j.Marker;
+import org.slf4j.event.DefaultLoggingEvent;
+import org.slf4j.event.Level;
+import org.slf4j.event.LoggingEvent;
+import org.slf4j.helpers.AbstractLogger;
+
+import java.util.ArrayDeque;
+import java.util.List;
+import java.util.Objects;
+import java.util.Queue;
+
+class EventualLogger extends AbstractLogger {
+
+ static final EventualLogger INSTANCE = new EventualLogger();
+
+ private final Queue