Browse Source

Merge pull request #3199 from cryptomator/feature/vault-volume-type

Feature: Customizable Volume Type Options for Each Vault
mindmonk 1 year ago
parent
commit
923af4bc83

+ 5 - 9
src/main/java/org/cryptomator/common/CommonsModule.java

@@ -5,10 +5,8 @@
  *******************************************************************************/
 package org.cryptomator.common;
 
-import com.tobiasdiez.easybind.EasyBind;
 import dagger.Module;
 import dagger.Provides;
-import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.common.keychain.KeychainModule;
 import org.cryptomator.common.mount.MountModule;
 import org.cryptomator.common.settings.Settings;
@@ -16,14 +14,13 @@ import org.cryptomator.common.settings.SettingsProvider;
 import org.cryptomator.common.vaults.VaultComponent;
 import org.cryptomator.common.vaults.VaultListModule;
 import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
+import org.cryptomator.integrations.mount.MountService;
 import org.cryptomator.integrations.revealpath.RevealPathService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Named;
 import javax.inject.Singleton;
-import javafx.beans.value.ObservableValue;
-import java.net.InetSocketAddress;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.util.Comparator;
@@ -33,6 +30,7 @@ import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 
 @Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class, MountModule.class})
 public abstract class CommonsModule {
@@ -138,11 +136,9 @@ public abstract class CommonsModule {
 
 	@Provides
 	@Singleton
-	static ObservableValue<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
-		return settings.port.map(port -> {
-			String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
-			return InetSocketAddress.createUnresolved(host, settings.port.intValue());
-		});
+	@Named("FUPFMS")
+	static AtomicReference<MountService> provideFirstUsedProblematicFuseMountService() {
+		return new AtomicReference<>(null);
 	}
 
 }

+ 0 - 6
src/main/java/org/cryptomator/common/mount/ActualMountService.java

@@ -1,6 +0,0 @@
-package org.cryptomator.common.mount;
-
-import org.cryptomator.integrations.mount.MountService;
-
-public record ActualMountService(MountService service, boolean isDesired) {
-}

+ 10 - 0
src/main/java/org/cryptomator/common/mount/FuseRestartRequiredException.java

@@ -0,0 +1,10 @@
+package org.cryptomator.common.mount;
+
+import org.cryptomator.integrations.mount.MountFailedException;
+
+public class FuseRestartRequiredException extends MountFailedException {
+
+	public FuseRestartRequiredException(String msg) {
+		super(msg);
+	}
+}

+ 5 - 46
src/main/java/org/cryptomator/common/mount/MountModule.java

@@ -4,21 +4,15 @@ import dagger.Module;
 import dagger.Provides;
 import org.cryptomator.common.ObservableUtil;
 import org.cryptomator.common.settings.Settings;
-import org.cryptomator.integrations.mount.Mount;
 import org.cryptomator.integrations.mount.MountService;
 
-import javax.inject.Named;
 import javax.inject.Singleton;
 import javafx.beans.value.ObservableValue;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicReference;
 
 @Module
 public class MountModule {
 
-	private static final AtomicReference<MountService> formerSelectedMountService = new AtomicReference<>(null);
-	private static final List<String> problematicFuseMountServices = List.of("org.cryptomator.frontend.fuse.mount.MacFuseMountProvider", "org.cryptomator.frontend.fuse.mount.FuseTMountProvider");
-
 	@Provides
 	@Singleton
 	static List<MountService> provideSupportedMountServices() {
@@ -27,46 +21,11 @@ public class MountModule {
 
 	@Provides
 	@Singleton
-	@Named("FUPFMS")
-	static AtomicReference<MountService> provideFirstUsedProblematicFuseMountService() {
-		return new AtomicReference<>(null);
-	}
-
-	@Provides
-	@Singleton
-	static ObservableValue<ActualMountService> provideMountService(Settings settings, List<MountService> serviceImpls, @Named("FUPFMS") AtomicReference<MountService> fupfms) {
-		var fallbackProvider = serviceImpls.stream().findFirst().orElse(null);
-
-		var observableMountService = ObservableUtil.mapWithDefault(settings.mountService, //
-				desiredServiceImpl -> { //
-					var serviceFromSettings = serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(desiredServiceImpl)).findAny(); //
-					var targetedService = serviceFromSettings.orElse(fallbackProvider);
-					return applyWorkaroundForProblematicFuse(targetedService, serviceFromSettings.isPresent(), fupfms);
-				}, //
-				() -> { //
-					return applyWorkaroundForProblematicFuse(fallbackProvider, true, fupfms);
-				});
-		return observableMountService;
+	static ObservableValue<MountService> provideDefaultMountService(List<MountService> mountProviders, Settings settings) {
+		var fallbackProvider = mountProviders.stream().findFirst().get(); //there should always be a mount provider, at least webDAV
+		return ObservableUtil.mapWithDefault(settings.mountService, //
+				serviceName -> mountProviders.stream().filter(s -> s.getClass().getName().equals(serviceName)).findFirst().orElse(fallbackProvider), //
+				fallbackProvider);
 	}
 
-	//see https://github.com/cryptomator/cryptomator/issues/2786
-	private synchronized static ActualMountService applyWorkaroundForProblematicFuse(MountService targetedService, boolean isDesired, AtomicReference<MountService> firstUsedProblematicFuseMountService) {
-		//set the first used problematic fuse service if applicable
-		var targetIsProblematicFuse = isProblematicFuseService(targetedService);
-		if (targetIsProblematicFuse && firstUsedProblematicFuseMountService.get() == null) {
-			firstUsedProblematicFuseMountService.set(targetedService);
-		}
-
-		//do not use the targeted mount service and fallback to former one, if the service is problematic _and_ not the first problematic one used.
-		if (targetIsProblematicFuse && !firstUsedProblematicFuseMountService.get().equals(targetedService)) {
-			return new ActualMountService(formerSelectedMountService.get(), false);
-		} else {
-			formerSelectedMountService.set(targetedService);
-			return new ActualMountService(targetedService, isDesired);
-		}
-	}
-
-	public static boolean isProblematicFuseService(MountService service) {
-		return problematicFuseMountServices.contains(service.getClass().getName());
-	}
 }

+ 44 - 14
src/main/java/org/cryptomator/common/mount/Mounter.java

@@ -9,11 +9,14 @@ import org.cryptomator.integrations.mount.MountFailedException;
 import org.cryptomator.integrations.mount.MountService;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 import javax.inject.Singleton;
 import javafx.beans.value.ObservableValue;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER;
 import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR;
@@ -24,24 +27,34 @@ import static org.cryptomator.integrations.mount.MountCapability.UNMOUNT_FORCED;
 @Singleton
 public class Mounter {
 
-	private final Settings settings;
+	private static final List<String> CONFLICTING_MOUNT_SERVICES = List.of("org.cryptomator.frontend.fuse.mount.MacFuseMountProvider", "org.cryptomator.frontend.fuse.mount.FuseTMountProvider");
 	private final Environment env;
+	private final Settings settings;
 	private final WindowsDriveLetters driveLetters;
-	private final ObservableValue<ActualMountService> mountServiceObservable;
+	private final List<MountService> mountProviders;
+	private final AtomicReference<MountService> firstUsedProblematicFuseMountService;
+	private final ObservableValue<MountService> defaultMountService;
 
 	@Inject
-	public Mounter(Settings settings, Environment env, WindowsDriveLetters driveLetters, ObservableValue<ActualMountService> mountServiceObservable) {
-		this.settings = settings;
+	public Mounter(Environment env, //
+				   Settings settings, //
+				   WindowsDriveLetters driveLetters, //
+				   List<MountService> mountProviders, //
+				   @Named("FUPFMS") AtomicReference<MountService> firstUsedProblematicFuseMountService, //
+				   ObservableValue<MountService> defaultMountService) {
 		this.env = env;
+		this.settings = settings;
 		this.driveLetters = driveLetters;
-		this.mountServiceObservable = mountServiceObservable;
+		this.mountProviders = mountProviders;
+		this.firstUsedProblematicFuseMountService = firstUsedProblematicFuseMountService;
+		this.defaultMountService = defaultMountService;
 	}
 
 	private class SettledMounter {
 
-		private MountService service;
-		private MountBuilder builder;
-		private VaultSettings vaultSettings;
+		private final MountService service;
+		private final MountBuilder builder;
+		private final VaultSettings vaultSettings;
 
 		public SettledMounter(MountService service, MountBuilder builder, VaultSettings vaultSettings) {
 			this.service = service;
@@ -53,8 +66,13 @@ public class Mounter {
 			for (var capability : service.capabilities()) {
 				switch (capability) {
 					case FILE_SYSTEM_NAME -> builder.setFileSystemName("cryptoFs");
-					case LOOPBACK_PORT ->
-							builder.setLoopbackPort(settings.port.get()); //TODO: move port from settings to vaultsettings (see https://github.com/cryptomator/cryptomator/tree/feature/mount-setting-per-vault)
+					case LOOPBACK_PORT -> {
+						if (vaultSettings.mountService.getValue() == null) {
+							builder.setLoopbackPort(settings.port.get());
+						} else {
+							builder.setLoopbackPort(vaultSettings.port.get());
+						}
+					}
 					case LOOPBACK_HOST_NAME -> env.getLoopbackAlias().ifPresent(builder::setLoopbackHostName);
 					case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode.get());
 					case MOUNT_FLAGS -> {
@@ -131,11 +149,23 @@ public class Mounter {
 	}
 
 	public MountHandle mount(VaultSettings vaultSettings, Path cryptoFsRoot) throws IOException, MountFailedException {
-		var mountService = this.mountServiceObservable.getValue().service();
-		var builder = mountService.forFileSystem(cryptoFsRoot);
-		var internal = new SettledMounter(mountService, builder, vaultSettings);
+		var selMntServ = mountProviders.stream().filter(s -> s.getClass().getName().equals(vaultSettings.mountService.getValue())).findFirst().orElse(defaultMountService.getValue());
+
+		var targetIsProblematicFuse = isProblematicFuseService(selMntServ);
+		if (targetIsProblematicFuse && firstUsedProblematicFuseMountService.get() == null) {
+			firstUsedProblematicFuseMountService.set(selMntServ);
+		} else if (targetIsProblematicFuse && !firstUsedProblematicFuseMountService.get().equals(selMntServ)) {
+			throw new FuseRestartRequiredException("Failed to mount the specified mount service.");
+		}
+
+		var builder = selMntServ.forFileSystem(cryptoFsRoot);
+		var internal = new SettledMounter(selMntServ, builder, vaultSettings);
 		var cleanup = internal.prepare();
-		return new MountHandle(builder.mount(), mountService.hasCapability(UNMOUNT_FORCED), cleanup);
+		return new MountHandle(builder.mount(), selMntServ.hasCapability(UNMOUNT_FORCED), cleanup);
+	}
+
+	public static boolean isProblematicFuseService(MountService service) {
+		return CONFLICTING_MOUNT_SERVICES.contains(service.getClass().getName());
 	}
 
 	public record MountHandle(Mount mountObj, boolean supportsUnmountForced, Runnable specialCleanup) {

+ 8 - 2
src/main/java/org/cryptomator/common/settings/VaultSettings.java

@@ -8,7 +8,6 @@ package org.cryptomator.common.settings;
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import com.google.common.io.BaseEncoding;
-import org.apache.commons.lang3.SystemUtils;
 import org.jetbrains.annotations.VisibleForTesting;
 
 import javafx.beans.Observable;
@@ -40,6 +39,7 @@ public class VaultSettings {
 	static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
 	static final boolean DEFAULT_AUTOLOCK_WHEN_IDLE = false;
 	static final int DEFAULT_AUTOLOCK_IDLE_SECONDS = 30 * 60;
+	static final int DEFAULT_PORT = 42427;
 
 	private static final Random RNG = new Random();
 
@@ -56,6 +56,8 @@ public class VaultSettings {
 	public final IntegerProperty autoLockIdleSeconds;
 	public final ObjectProperty<Path> mountPoint;
 	public final StringExpression mountName;
+	public final StringProperty mountService;
+	public final IntegerProperty port;
 
 	VaultSettings(VaultSettingsJson json) {
 		this.id = json.id;
@@ -70,6 +72,8 @@ public class VaultSettings {
 		this.autoLockWhenIdle = new SimpleBooleanProperty(this, "autoLockWhenIdle", json.autoLockWhenIdle);
 		this.autoLockIdleSeconds = new SimpleIntegerProperty(this, "autoLockIdleSeconds", json.autoLockIdleSeconds);
 		this.mountPoint = new SimpleObjectProperty<>(this, "mountPoint", json.mountPoint == null ? null : Path.of(json.mountPoint));
+		this.mountService = new SimpleStringProperty(this, "mountService", json.mountService);
+		this.port = new SimpleIntegerProperty(this, "port", json.port);
 		// mount name is no longer an explicit setting, see https://github.com/cryptomator/cryptomator/pull/1318
 		this.mountName = StringExpression.stringExpression(Bindings.createStringBinding(() -> {
 			final String name;
@@ -95,7 +99,7 @@ public class VaultSettings {
 	}
 
 	Observable[] observables() {
-		return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode};
+		return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode, port, mountService};
 	}
 
 	public static VaultSettings withRandomId() {
@@ -124,6 +128,8 @@ public class VaultSettings {
 		json.autoLockWhenIdle = autoLockWhenIdle.get();
 		json.autoLockIdleSeconds = autoLockIdleSeconds.get();
 		json.mountPoint = mountPoint.map(Path::toString).getValue();
+		json.mountService = mountService.get();
+		json.port = port.get();
 		return json;
 	}
 

+ 6 - 0
src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java

@@ -45,6 +45,12 @@ class VaultSettingsJson {
 	@JsonProperty("autoLockIdleSeconds")
 	int autoLockIdleSeconds = VaultSettings.DEFAULT_AUTOLOCK_IDLE_SECONDS;
 
+	@JsonProperty("mountService")
+	String mountService;
+
+	@JsonProperty("port")
+	int port = VaultSettings.DEFAULT_PORT;
+
 	@Deprecated(since = "1.7.0")
 	@JsonProperty(value = "winDriveLetter", access = JsonProperty.Access.WRITE_ONLY) // WRITE_ONLY means value is "written" into the java object during deserialization. Upvote this: https://github.com/FasterXML/jackson-annotations/issues/233
 	String winDriveLetter;

+ 7 - 2
src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -11,7 +11,6 @@ package org.cryptomator.common.vaults;
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.common.Constants;
 import org.cryptomator.common.mount.Mounter;
-import org.cryptomator.common.mount.WindowsDriveLetters;
 import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.cryptofs.CryptoFileSystem;
 import org.cryptomator.cryptofs.CryptoFileSystemProperties;
@@ -73,7 +72,13 @@ public class Vault {
 	private final AtomicReference<Mounter.MountHandle> mountHandle = new AtomicReference<>(null);
 
 	@Inject
-	Vault(VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats, WindowsDriveLetters windowsDriveLetters, Mounter mounter) {
+	Vault(VaultSettings vaultSettings, //
+		  VaultConfigCache configCache, //
+		  AtomicReference<CryptoFileSystem> cryptoFileSystem, //
+		  VaultState state, //
+		  @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, //
+		  VaultStats stats, //
+		  Mounter mounter) {
 		this.vaultSettings = vaultSettings;
 		this.configCache = configCache;
 		this.cryptoFileSystem = cryptoFileSystem;

+ 1 - 0
src/main/java/org/cryptomator/ui/common/FxmlFile.java

@@ -45,6 +45,7 @@ public enum FxmlFile {
 	REMOVE_VAULT("/fxml/remove_vault.fxml"), //
 	UPDATE_REMINDER("/fxml/update_reminder.fxml"), //
 	UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
+	UNLOCK_FUSE_RESTART_REQUIRED("/fxml/unlock_fuse_restart_required.fxml"), //
 	UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
 	UNLOCK_SELECT_MASTERKEYFILE("/fxml/unlock_select_masterkeyfile.fxml"), //
 	UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //

+ 11 - 27
src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java

@@ -2,17 +2,14 @@ package org.cryptomator.ui.preferences;
 
 import dagger.Lazy;
 import org.cryptomator.common.ObservableUtil;
-import org.cryptomator.common.mount.MountModule;
 import org.cryptomator.common.settings.Settings;
 import org.cryptomator.integrations.mount.MountCapability;
 import org.cryptomator.integrations.mount.MountService;
 import org.cryptomator.ui.common.FxController;
 
 import javax.inject.Inject;
-import javax.inject.Named;
 import javafx.application.Application;
 import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanExpression;
 import javafx.beans.value.ObservableValue;
 import javafx.scene.control.Button;
 import javafx.scene.control.ChoiceBox;
@@ -21,24 +18,22 @@ import javafx.util.StringConverter;
 import java.util.List;
 import java.util.Optional;
 import java.util.ResourceBundle;
-import java.util.concurrent.atomic.AtomicReference;
 
 @PreferencesScoped
 public class VolumePreferencesController implements FxController {
 
-	private static final String DOCS_MOUNTING_URL = "https://docs.cryptomator.org/en/1.7/desktop/volume-type/";
-	private static final int MIN_PORT = 1024;
-	private static final int MAX_PORT = 65535;
+	public static final String DOCS_MOUNTING_URL = "https://docs.cryptomator.org/en/1.7/desktop/volume-type/";
+	public static final int MIN_PORT = 1024;
+	public static final int MAX_PORT = 65535;
 
 	private final Settings settings;
 	private final ObservableValue<MountService> selectedMountService;
 	private final ResourceBundle resourceBundle;
-	private final BooleanExpression loopbackPortSupported;
+	private final ObservableValue<Boolean> loopbackPortSupported;
 	private final ObservableValue<Boolean> mountToDirSupported;
 	private final ObservableValue<Boolean> mountToDriveLetterSupported;
 	private final ObservableValue<Boolean> mountFlagsSupported;
 	private final ObservableValue<Boolean> readonlySupported;
-	private final ObservableValue<Boolean> fuseRestartRequired;
 	private final Lazy<Application> application;
 	private final List<MountService> mountProviders;
 	public ChoiceBox<MountService> volumeTypeChoiceBox;
@@ -46,7 +41,10 @@ public class VolumePreferencesController implements FxController {
 	public Button loopbackPortApplyButton;
 
 	@Inject
-	VolumePreferencesController(Settings settings, Lazy<Application> application, List<MountService> mountProviders, @Named("FUPFMS") AtomicReference<MountService> firstUsedProblematicFuseMountService, ResourceBundle resourceBundle) {
+	VolumePreferencesController(Settings settings, //
+								Lazy<Application> application, //
+								List<MountService> mountProviders, //
+								ResourceBundle resourceBundle) {
 		this.settings = settings;
 		this.application = application;
 		this.mountProviders = mountProviders;
@@ -54,17 +52,11 @@ public class VolumePreferencesController implements FxController {
 
 		var fallbackProvider = mountProviders.stream().findFirst().orElse(null);
 		this.selectedMountService = ObservableUtil.mapWithDefault(settings.mountService, serviceName -> mountProviders.stream().filter(s -> s.getClass().getName().equals(serviceName)).findFirst().orElse(fallbackProvider), fallbackProvider);
-		this.loopbackPortSupported = BooleanExpression.booleanExpression(selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT)));
+		this.loopbackPortSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT));
 		this.mountToDirSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT) || s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR));
 		this.mountToDriveLetterSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
 		this.mountFlagsSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS));
 		this.readonlySupported = selectedMountService.map(s -> s.hasCapability(MountCapability.READ_ONLY));
-		this.fuseRestartRequired = selectedMountService.map(s -> {//
-			return firstUsedProblematicFuseMountService.get() != null //
-					&& MountModule.isProblematicFuseService(s) //
-					&& !firstUsedProblematicFuseMountService.get().equals(s);
-		});
-
 	}
 
 	public void initialize() {
@@ -101,12 +93,12 @@ public class VolumePreferencesController implements FxController {
 
 	/* Property Getters */
 
-	public BooleanExpression loopbackPortSupportedProperty() {
+	public ObservableValue<Boolean> loopbackPortSupportedProperty() {
 		return loopbackPortSupported;
 	}
 
 	public boolean isLoopbackPortSupported() {
-		return loopbackPortSupported.get();
+		return loopbackPortSupported.getValue();
 	}
 
 	public ObservableValue<Boolean> readonlySupportedProperty() {
@@ -141,14 +133,6 @@ public class VolumePreferencesController implements FxController {
 		return mountFlagsSupported.getValue();
 	}
 
-	public ObservableValue<Boolean> fuseRestartRequiredProperty() {
-		return fuseRestartRequired;
-	}
-
-	public boolean getFuseRestartRequired() {
-		return fuseRestartRequired.getValue();
-	}
-
 	/* Helpers */
 
 	private class MountServiceConverter extends StringConverter<MountService> {

+ 47 - 0
src/main/java/org/cryptomator/ui/unlock/UnlockFuseRestartRequiredController.java

@@ -0,0 +1,47 @@
+package org.cryptomator.ui.unlock;
+
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.fxapp.FxApplicationWindows;
+import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
+
+import javax.inject.Inject;
+import javafx.fxml.FXML;
+import javafx.stage.Stage;
+import java.util.ResourceBundle;
+
+@UnlockScoped
+public class UnlockFuseRestartRequiredController implements FxController {
+
+	private final Stage window;
+	private final ResourceBundle resourceBundle;
+	private final FxApplicationWindows appWindows;
+	private final Vault vault;
+
+	@Inject
+	UnlockFuseRestartRequiredController(@UnlockWindow Stage window, //
+										ResourceBundle resourceBundle, //
+										FxApplicationWindows appWindows, //
+										@UnlockWindow Vault vault) {
+		this.window = window;
+		this.resourceBundle = resourceBundle;
+		this.appWindows = appWindows;
+		this.vault = vault;
+	}
+
+	public void initialize() {
+		window.setTitle(String.format(resourceBundle.getString("unlock.error.title"), vault.getDisplayName()));
+	}
+
+	@FXML
+	public void close() {
+		window.close();
+	}
+
+	@FXML
+	public void closeAndOpenVaultOptions() {
+		appWindows.showVaultOptionsWindow(vault, SelectedVaultOptionsTab.MOUNT);
+		window.close();
+	}
+
+}

+ 12 - 0
src/main/java/org/cryptomator/ui/unlock/UnlockModule.java

@@ -81,6 +81,13 @@ abstract class UnlockModule {
 		return fxmlLoaders.createScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT);
 	}
 
+	@Provides
+	@FxmlScene(FxmlFile.UNLOCK_FUSE_RESTART_REQUIRED)
+	@UnlockScoped
+	static Scene provideFuseRestartRequiredScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) {
+		return fxmlLoaders.createScene(FxmlFile.UNLOCK_FUSE_RESTART_REQUIRED);
+	}
+
 	// ------------------
 
 	@Binds
@@ -93,4 +100,9 @@ abstract class UnlockModule {
 	@FxControllerKey(UnlockInvalidMountPointController.class)
 	abstract FxController bindUnlockInvalidMountPointController(UnlockInvalidMountPointController controller);
 
+	@Binds
+	@IntoMap
+	@FxControllerKey(UnlockFuseRestartRequiredController.class)
+	abstract FxController bindUnlockFuseRestartRequiredController(UnlockFuseRestartRequiredController controller);
+
 }

+ 34 - 14
src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java

@@ -1,6 +1,7 @@
 package org.cryptomator.ui.unlock;
 
 import dagger.Lazy;
+import org.cryptomator.common.mount.FuseRestartRequiredException;
 import org.cryptomator.common.mount.IllegalMountPointException;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.common.vaults.VaultState;
@@ -37,17 +38,27 @@ public class UnlockWorkflow extends Task<Void> {
 	private final VaultService vaultService;
 	private final Lazy<Scene> successScene;
 	private final Lazy<Scene> invalidMountPointScene;
+	private final Lazy<Scene> fuseRestartRequiredScene;
 	private final FxApplicationWindows appWindows;
 	private final KeyLoadingStrategy keyLoadingStrategy;
 	private final ObjectProperty<IllegalMountPointException> illegalMountPointException;
 
 	@Inject
-	UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, @UnlockWindow ObjectProperty<IllegalMountPointException> illegalMountPointException) {
+	UnlockWorkflow(@UnlockWindow Stage window, //
+				   @UnlockWindow Vault vault, //
+				   VaultService vaultService, //
+				   @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, //
+				   @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, //
+				   @FxmlScene(FxmlFile.UNLOCK_FUSE_RESTART_REQUIRED) Lazy<Scene> fuseRestartRequiredScene, //
+				   FxApplicationWindows appWindows, //
+				   @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, //
+				   @UnlockWindow ObjectProperty<IllegalMountPointException> illegalMountPointException) {
 		this.window = window;
 		this.vault = vault;
 		this.vaultService = vaultService;
 		this.successScene = successScene;
 		this.invalidMountPointScene = invalidMountPointScene;
+		this.fuseRestartRequiredScene = fuseRestartRequiredScene;
 		this.appWindows = appWindows;
 		this.keyLoadingStrategy = keyLoadingStrategy;
 		this.illegalMountPointException = illegalMountPointException;
@@ -68,6 +79,26 @@ public class UnlockWorkflow extends Task<Void> {
 		}
 	}
 
+	private void handleIllegalMountPointError(IllegalMountPointException impe) {
+		Platform.runLater(() -> {
+			illegalMountPointException.set(impe);
+			window.setScene(invalidMountPointScene.get());
+			window.show();
+		});
+	}
+
+	private void handleFuseRestartRequiredError() {
+		Platform.runLater(() -> {
+			window.setScene(fuseRestartRequiredScene.get());
+			window.show();
+		});
+	}
+
+	private void handleGenericError(Throwable e) {
+		LOG.error("Unlock failed for technical reasons.", e);
+		appWindows.showErrorWindow(e, window, null);
+	}
+
 	@Override
 	protected void succeeded() {
 		LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
@@ -93,25 +124,14 @@ public class UnlockWorkflow extends Task<Void> {
 		Throwable throwable = super.getException();
 		if(throwable instanceof IllegalMountPointException impe) {
 			handleIllegalMountPointError(impe);
+		} else if (throwable instanceof FuseRestartRequiredException _) {
+			handleFuseRestartRequiredError();
 		} else {
 			handleGenericError(throwable);
 		}
 		vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
 	}
 
-	private void handleIllegalMountPointError(IllegalMountPointException impe) {
-		Platform.runLater(() -> {
-			illegalMountPointException.set(impe);
-			window.setScene(invalidMountPointScene.get());
-			window.show();
-		});
-	}
-
-	private void handleGenericError(Throwable e) {
-		LOG.error("Unlock failed for technical reasons.", e);
-		appWindows.showErrorWindow(e, window, null);
-	}
-
 	@Override
 	protected void cancelled() {
 		LOG.debug("Unlock of '{}' canceled.", vault.getDisplayName());

+ 136 - 11
src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java

@@ -1,18 +1,26 @@
 package org.cryptomator.ui.vaultoptions;
 
 import com.google.common.base.Strings;
-import org.cryptomator.common.mount.ActualMountService;
+import dagger.Lazy;
+import org.cryptomator.common.ObservableUtil;
+import org.cryptomator.common.mount.Mounter;
 import org.cryptomator.common.mount.WindowsDriveLetters;
 import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.integrations.mount.MountCapability;
+import org.cryptomator.integrations.mount.MountService;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.fxapp.FxApplicationWindows;
 import org.cryptomator.ui.preferences.SelectedPreferencesTab;
+import org.cryptomator.ui.preferences.VolumePreferencesController;
 
 import javax.inject.Inject;
+import javax.inject.Named;
+import javafx.application.Application;
+import javafx.beans.binding.Bindings;
 import javafx.beans.value.ObservableValue;
 import javafx.fxml.FXML;
+import javafx.scene.control.Button;
 import javafx.scene.control.CheckBox;
 import javafx.scene.control.ChoiceBox;
 import javafx.scene.control.RadioButton;
@@ -26,8 +34,11 @@ import java.io.File;
 import java.nio.file.Files;
 import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
 import java.util.ResourceBundle;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
 
 @VaultOptionsScoped
 public class MountOptionsController implements FxController {
@@ -36,14 +47,21 @@ public class MountOptionsController implements FxController {
 	private final VaultSettings vaultSettings;
 	private final WindowsDriveLetters windowsDriveLetters;
 	private final ResourceBundle resourceBundle;
+	private final Lazy<Application> application;
 
 	private final ObservableValue<String> defaultMountFlags;
 	private final ObservableValue<Boolean> mountpointDirSupported;
 	private final ObservableValue<Boolean> mountpointDriveLetterSupported;
 	private final ObservableValue<Boolean> readOnlySupported;
 	private final ObservableValue<Boolean> mountFlagsSupported;
+	private final ObservableValue<Boolean> defaultMountServiceSelected;
 	private final ObservableValue<String> directoryPath;
 	private final FxApplicationWindows applicationWindows;
+	private final List<MountService> mountProviders;
+	private final ObservableValue<MountService> defaultMountService;
+	private final ObservableValue<MountService> selectedMountService;
+	private final ObservableValue<Boolean> fuseRestartRequired;
+	private final ObservableValue<Boolean> loopbackPortChangeable;
 
 
 	//-- FXML objects --
@@ -56,30 +74,62 @@ public class MountOptionsController implements FxController {
 	public RadioButton mountPointDirBtn;
 	public TextField directoryPathField;
 	public ChoiceBox<Path> driveLetterSelection;
+	public ChoiceBox<MountService> vaultVolumeTypeChoiceBox;
+	public TextField vaultLoopbackPortField;
+	public Button vaultLoopbackPortApplyButton;
+
 
 	@Inject
-	MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue<ActualMountService> mountService, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, FxApplicationWindows applicationWindows) {
+	MountOptionsController(@VaultOptionsWindow Stage window, //
+						   @VaultOptionsWindow Vault vault, //
+						   WindowsDriveLetters windowsDriveLetters, //
+						   ResourceBundle resourceBundle, //
+						   FxApplicationWindows applicationWindows, //
+						   Lazy<Application> application, //
+						   List<MountService> mountProviders, //
+						   @Named("FUPFMS") AtomicReference<MountService> firstUsedProblematicFuseMountService, //
+						   ObservableValue<MountService> defaultMountService) {
 		this.window = window;
 		this.vaultSettings = vault.getVaultSettings();
 		this.windowsDriveLetters = windowsDriveLetters;
 		this.resourceBundle = resourceBundle;
-		this.defaultMountFlags = mountService.map(as -> {
-			if (as.service().hasCapability(MountCapability.MOUNT_FLAGS)) {
-				return as.service().getDefaultMountFlags();
+		this.applicationWindows = applicationWindows;
+		this.directoryPath = vault.getVaultSettings().mountPoint.map(p -> isDriveLetter(p) ? null : p.toString());
+		this.application = application;
+		this.mountProviders = mountProviders;
+		this.defaultMountService = defaultMountService;
+		this.selectedMountService = Bindings.createObjectBinding(this::reselectMountService, defaultMountService, vaultSettings.mountService);
+		this.fuseRestartRequired = selectedMountService.map(s -> {
+			return firstUsedProblematicFuseMountService.get() != null //
+					&& Mounter.isProblematicFuseService(s) //
+					&& !firstUsedProblematicFuseMountService.get().equals(s);
+		});
+
+		this.defaultMountFlags = selectedMountService.map(s -> {
+			if (s.hasCapability(MountCapability.MOUNT_FLAGS)) {
+				return s.getDefaultMountFlags();
 			} else {
 				return "";
 			}
 		});
-		this.mountpointDirSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR) || as.service().hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT));
-		this.mountpointDriveLetterSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
-		this.mountFlagsSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_FLAGS));
-		this.readOnlySupported = mountService.map(as -> as.service().hasCapability(MountCapability.READ_ONLY));
-		this.directoryPath = vault.getVaultSettings().mountPoint.map(p -> isDriveLetter(p) ? null : p.toString());
-		this.applicationWindows = applicationWindows;
+		this.mountFlagsSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS));
+		this.defaultMountServiceSelected = ObservableUtil.mapWithDefault(vaultSettings.mountService, _ -> false, true);
+		this.readOnlySupported = selectedMountService.map(s -> s.hasCapability(MountCapability.READ_ONLY));
+		this.mountpointDirSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR) || s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT));
+		this.mountpointDriveLetterSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
+		this.loopbackPortChangeable = selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT) && vaultSettings.mountService.getValue() != null);
+	}
+
+	private MountService reselectMountService() {
+		var desired = vaultSettings.mountService.getValue();
+		var defaultMS = defaultMountService.getValue();
+		return mountProviders.stream().filter(s -> s.getClass().getName().equals(desired)).findFirst().orElse(defaultMS);
 	}
 
 	@FXML
 	public void initialize() {
+		defaultMountService.addListener((_, _, _) -> vaultVolumeTypeChoiceBox.setConverter(new MountServiceConverter()));
+
 		// readonly:
 		readOnlyCheckbox.selectedProperty().bindBidirectional(vaultSettings.usesReadOnlyMode);
 
@@ -106,6 +156,20 @@ public class MountOptionsController implements FxController {
 			mountPointToggleGroup.selectToggle(mountPointDirBtn);
 		}
 		mountPointToggleGroup.selectedToggleProperty().addListener(this::selectedToggleChanged);
+
+		vaultVolumeTypeChoiceBox.getItems().add(null);
+		vaultVolumeTypeChoiceBox.getItems().addAll(mountProviders);
+		vaultVolumeTypeChoiceBox.setConverter(new MountServiceConverter());
+		vaultVolumeTypeChoiceBox.getSelectionModel().select(isDefaultMountServiceSelected() ? null : selectedMountService.getValue());
+		vaultVolumeTypeChoiceBox.valueProperty().addListener((_, _, newProvider) -> {
+			var toSet = Optional.ofNullable(newProvider).map(nP -> nP.getClass().getName()).orElse(null);
+			vaultSettings.mountService.set(toSet);
+		});
+
+		vaultLoopbackPortField.setText(String.valueOf(vaultSettings.port.get()));
+		vaultLoopbackPortApplyButton.visibleProperty().bind(vaultSettings.port.asString().isNotEqualTo(vaultLoopbackPortField.textProperty()));
+		vaultLoopbackPortApplyButton.disableProperty().bind(Bindings.createBooleanBinding(this::validateLoopbackPort, vaultLoopbackPortField.textProperty()).not());
+
 	}
 
 	@FXML
@@ -229,6 +293,26 @@ public class MountOptionsController implements FxController {
 
 	}
 
+	public void openDocs() {
+		application.get().getHostServices().showDocument(VolumePreferencesController.DOCS_MOUNTING_URL);
+	}
+
+	private boolean validateLoopbackPort() {
+		try {
+			int port = Integer.parseInt(vaultLoopbackPortField.getText());
+			return port == 0 // choose port automatically
+					|| port >= VolumePreferencesController.MIN_PORT && port <= VolumePreferencesController.MAX_PORT; // port within range
+		} catch (NumberFormatException e) {
+			return false;
+		}
+	}
+
+	public void doChangeLoopbackPort() {
+		if (validateLoopbackPort()) {
+			vaultSettings.port.set(Integer.parseInt(vaultLoopbackPortField.getText()));
+		}
+	}
+
 	//@formatter:off
 	private static class NoDirSelectedException extends Exception {}
 	//@formatter:on
@@ -243,6 +327,14 @@ public class MountOptionsController implements FxController {
 		return mountFlagsSupported.getValue();
 	}
 
+	public ObservableValue<Boolean> defaultMountServiceSelectedProperty() {
+		return defaultMountServiceSelected;
+	}
+
+	public boolean isDefaultMountServiceSelected() {
+		return defaultMountServiceSelected.getValue();
+	}
+
 	public ObservableValue<Boolean> mountpointDirSupportedProperty() {
 		return mountpointDirSupported;
 	}
@@ -274,4 +366,37 @@ public class MountOptionsController implements FxController {
 	public String getDirectoryPath() {
 		return directoryPath.getValue();
 	}
+
+	public ObservableValue<Boolean> fuseRestartRequiredProperty() {
+		return fuseRestartRequired;
+	}
+
+	public boolean getFuseRestartRequired() {
+		return fuseRestartRequired.getValue();
+	}
+
+	public ObservableValue<Boolean> loopbackPortChangeableProperty() {
+		return loopbackPortChangeable;
+	}
+
+	public boolean isLoopbackPortChangeable() {
+		return loopbackPortChangeable.getValue();
+	}
+
+	private class MountServiceConverter extends StringConverter<MountService> {
+
+		@Override
+		public String toString(MountService provider) {
+			if (provider == null) {
+				return String.format(resourceBundle.getString("vaultOptions.mount.volumeType.default"), defaultMountService.getValue().displayName());
+			} else {
+				return provider.displayName();
+			}
+		}
+
+		@Override
+		public MountService fromString(String string) {
+			throw new UnsupportedOperationException();
+		}
+	}
 }

+ 0 - 2
src/main/resources/fxml/preferences_volume.fxml

@@ -32,8 +32,6 @@
 			</Hyperlink>
 		</HBox>
 
-		<Label styleClass="label-red" text="%preferences.volume.fuseRestartRequired" visible="${controller.fuseRestartRequired}" managed="${controller.fuseRestartRequired}"/>
-
 		<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.loopbackPortSupported}" managed="${controller.loopbackPortSupported}">
 			<Label text="%preferences.volume.tcp.port"/>
 			<NumericTextField fx:id="loopbackPortField"/>

+ 58 - 0
src/main/resources/fxml/unlock_fuse_restart_required.fxml

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ButtonBar?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.control.Tooltip?>
+<?import javafx.scene.Group?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.Region?>
+<?import javafx.scene.layout.StackPane?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.shape.Circle?>
+<HBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.unlock.UnlockFuseRestartRequiredController"
+	  minWidth="400"
+	  maxWidth="400"
+	  minHeight="145"
+	  spacing="12"
+	  alignment="TOP_LEFT">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<children>
+		<Group>
+			<StackPane>
+				<padding>
+					<Insets topRightBottomLeft="6"/>
+				</padding>
+				<Circle styleClass="glyph-icon-red" radius="24"/>
+				<FontAwesome5IconView styleClass="glyph-icon-white" glyph="TIMES" glyphSize="24"/>
+			</StackPane>
+		</Group>
+		<VBox HBox.hgrow="ALWAYS">
+			<Label styleClass="label-large" text="%unlock.error.fuseRestartRequired.message" wrapText="true" textAlignment="LEFT">
+				<padding>
+					<Insets bottom="6" top="6"/>
+				</padding>
+			</Label>
+
+			<Label text="%unlock.error.fuseRestartRequired.description" wrapText="true" textAlignment="LEFT"/>
+
+			<Region VBox.vgrow="ALWAYS" minHeight="18"/>
+			<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
+				<buttons>
+					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
+					<Button text="%main.vaultlist.contextMenu.vaultoptions" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#closeAndOpenVaultOptions">
+						<tooltip>
+							<Tooltip text="%main.vaultlist.contextMenu.vaultoptions"/>
+						</tooltip>
+					</Button>
+				</buttons>
+			</ButtonBar>
+		</VBox>
+	</children>
+</HBox>

+ 31 - 6
src/main/resources/fxml/vault_options_mount.fxml

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import org.cryptomator.ui.controls.NumericTextField?>
 <?import javafx.geometry.Insets?>
 <?import javafx.scene.control.Button?>
 <?import javafx.scene.control.CheckBox?>
@@ -10,9 +11,9 @@
 <?import javafx.scene.control.RadioButton?>
 <?import javafx.scene.control.TextField?>
 <?import javafx.scene.control.ToggleGroup?>
+<?import javafx.scene.control.Tooltip?>
 <?import javafx.scene.layout.HBox?>
 <?import javafx.scene.layout.VBox?>
-<?import javafx.scene.text.TextFlow?>
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.vaultoptions.MountOptionsController"
@@ -24,11 +25,35 @@
 		<Insets topRightBottomLeft="12"/>
 	</padding>
 	<children>
-		<TextFlow>
-			<Label text="%vaultOptions.mount.info"/>
-			<Label text=" "/>
-			<Hyperlink styleClass="hyperlink-underline" text="%vaultOptions.mount.linkToPreferences" onAction="#openVolumePreferences" wrapText="true"/>
-		</TextFlow>
+		<HBox spacing="12" alignment="CENTER_LEFT">
+			<Label text="%vaultOptions.mount.volume.type"/>
+			<ChoiceBox fx:id="vaultVolumeTypeChoiceBox"/>
+			<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openVolumePreferences" visible="${controller.defaultMountServiceSelected}" managed="${controller.defaultMountServiceSelected}">
+				<graphic>
+					<FontAwesome5IconView glyph="COGS" styleClass="glyph-icon-muted"/>
+				</graphic>
+				<tooltip>
+					<Tooltip text="%vaultOptions.mount.info" showDelay="100ms"/>
+				</tooltip>
+			</Hyperlink>
+			<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs" visible="${!controller.defaultMountServiceSelected}" managed="${!controller.defaultMountServiceSelected}">
+				<graphic>
+					<FontAwesome5IconView glyph="QUESTION_CIRCLE" styleClass="glyph-icon-muted"/>
+				</graphic>
+				<tooltip>
+					<Tooltip text="%preferences.volume.docsTooltip" showDelay="100ms"/>
+				</tooltip>
+			</Hyperlink>
+		</HBox>
+
+		<Label styleClass="label-red" text="%vaultOptions.mount.volumeType.fuseRestartRequired" visible="${controller.fuseRestartRequired}" managed="${controller.fuseRestartRequired}"/>
+
+		<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.loopbackPortChangeable}" managed="${controller.loopbackPortChangeable}">
+			<Label text="%vaultOptions.mount.volume.tcp.port"/>
+			<NumericTextField fx:id="vaultLoopbackPortField"/>
+			<Button text="%generic.button.apply" fx:id="vaultLoopbackPortApplyButton" onAction="#doChangeLoopbackPort"/>
+		</HBox>
+
 		<CheckBox fx:id="readOnlyCheckbox" text="%vaultOptions.mount.readonly" visible="${controller.readOnlySupported}" managed="${controller.readOnlySupported}"/>
 
 		<VBox visible="${controller.mountFlagsSupported}" managed="${controller.mountFlagsSupported}">

+ 10 - 4
src/main/resources/i18n/strings.properties

@@ -142,6 +142,9 @@ unlock.error.customPath.description.hideawayNotDir=The temporary, hidden file "%
 unlock.error.customPath.description.couldNotBeCleaned=Your vault could not be mounted to the path "%s". Please try again or choose a different path.
 unlock.error.customPath.description.notEmptyDir=The custom mount path "%s" is not an empty folder. Please choose an empty folder and try again.
 unlock.error.customPath.description.generic=You have selected a custom mount path for this vault, but using it failed with the message: %2$s
+unlock.error.fuseRestartRequired.message=Unable to unlock vault
+unlock.error.fuseRestartRequired.description=Change the volume type in vault options or restart Cryptomator.
+unlock.error.title=Unlock "%s" failed
 ## Hub
 hub.noKeychain.message=Unable to access device key
 hub.noKeychain.description=In order to unlock Hub vaults, a device key is required, which is secured using a keychain. To proceed, enable “%s” and select a keychain in the preferences.
@@ -295,11 +298,11 @@ preferences.interface.showMinimizeButton=Show minimize button
 preferences.interface.showTrayIcon=Show tray icon (requires restart)
 ## Volume
 preferences.volume=Virtual Drive
-preferences.volume.type=Volume Type
+preferences.volume.type=Default Volume Type
 preferences.volume.type.automatic=Automatic
 preferences.volume.docsTooltip=Open the documentation to learn more about the different volume types.
 preferences.volume.fuseRestartRequired=To apply the changes, Cryptomator needs to be restarted.
-preferences.volume.tcp.port=TCP Port
+preferences.volume.tcp.port=Default TCP Port
 preferences.volume.supportedFeatures=The chosen volume type supports the following features:
 preferences.volume.feature.mountAuto=Automatic mount point selection
 preferences.volume.feature.mountToDir=Custom directory as mount point
@@ -438,8 +441,7 @@ vaultOptions.general.startHealthCheckBtn=Start Health Check
 
 ## Mount
 vaultOptions.mount=Mounting
-vaultOptions.mount.info=Options depend on the selected volume type.
-vaultOptions.mount.linkToPreferences=Open virtual drive preferences
+vaultOptions.mount.info=Open virtual drive preferences to change default settings.
 vaultOptions.mount.readonly=Read-only
 vaultOptions.mount.customMountFlags=Custom mount flags
 vaultOptions.mount.winDriveLetterOccupied=occupied
@@ -449,6 +451,10 @@ vaultOptions.mount.mountPoint.driveLetter=Use assigned drive letter
 vaultOptions.mount.mountPoint.custom=Use chosen directory
 vaultOptions.mount.mountPoint.directoryPickerButton=Choose…
 vaultOptions.mount.mountPoint.directoryPickerTitle=Pick a directory
+vaultOptions.mount.volumeType.default=Default (%s)
+vaultOptions.mount.volumeType.fuseRestartRequired=To use this volume type, Cryptomator needs to be restarted.
+vaultOptions.mount.volume.tcp.port=TCP Port
+vaultOptions.mount.volume.type=Volume Type
 ## Master Key
 vaultOptions.masterkey=Password
 vaultOptions.masterkey.changePasswordBtn=Change Password