diff --git a/src/main/java/org/cryptomator/common/CommonsModule.java b/src/main/java/org/cryptomator/common/CommonsModule.java index 4ac07495e..5ea69da6d 100644 --- a/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/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 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 provideFirstUsedProblematicFuseMountService() { + return new AtomicReference<>(null); } } diff --git a/src/main/java/org/cryptomator/common/mount/ActualMountService.java b/src/main/java/org/cryptomator/common/mount/ActualMountService.java deleted file mode 100644 index a96cc8e37..000000000 --- a/src/main/java/org/cryptomator/common/mount/ActualMountService.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.cryptomator.common.mount; - -import org.cryptomator.integrations.mount.MountService; - -public record ActualMountService(MountService service, boolean isDesired) { -} diff --git a/src/main/java/org/cryptomator/common/mount/FuseRestartRequiredException.java b/src/main/java/org/cryptomator/common/mount/FuseRestartRequiredException.java new file mode 100644 index 000000000..5e4420889 --- /dev/null +++ b/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); + } +} diff --git a/src/main/java/org/cryptomator/common/mount/MountModule.java b/src/main/java/org/cryptomator/common/mount/MountModule.java index cbcb23e82..3b6fa63c7 100644 --- a/src/main/java/org/cryptomator/common/mount/MountModule.java +++ b/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 formerSelectedMountService = new AtomicReference<>(null); - private static final List problematicFuseMountServices = List.of("org.cryptomator.frontend.fuse.mount.MacFuseMountProvider", "org.cryptomator.frontend.fuse.mount.FuseTMountProvider"); - @Provides @Singleton static List provideSupportedMountServices() { @@ -27,46 +21,11 @@ public class MountModule { @Provides @Singleton - @Named("FUPFMS") - static AtomicReference provideFirstUsedProblematicFuseMountService() { - return new AtomicReference<>(null); + static ObservableValue provideDefaultMountService(List 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); } - @Provides - @Singleton - static ObservableValue provideMountService(Settings settings, List serviceImpls, @Named("FUPFMS") AtomicReference 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; - } - - //see https://github.com/cryptomator/cryptomator/issues/2786 - private synchronized static ActualMountService applyWorkaroundForProblematicFuse(MountService targetedService, boolean isDesired, AtomicReference 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()); - } } diff --git a/src/main/java/org/cryptomator/common/mount/Mounter.java b/src/main/java/org/cryptomator/common/mount/Mounter.java index 101524ea3..bf9fb5b1f 100644 --- a/src/main/java/org/cryptomator/common/mount/Mounter.java +++ b/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 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 mountServiceObservable; + private final List mountProviders; + private final AtomicReference firstUsedProblematicFuseMountService; + private final ObservableValue defaultMountService; @Inject - public Mounter(Settings settings, Environment env, WindowsDriveLetters driveLetters, ObservableValue mountServiceObservable) { - this.settings = settings; + public Mounter(Environment env, // + Settings settings, // + WindowsDriveLetters driveLetters, // + List mountProviders, // + @Named("FUPFMS") AtomicReference firstUsedProblematicFuseMountService, // + ObservableValue 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) { diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 6662f61ff..fd21fc197 100644 --- a/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/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 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; } diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java b/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java index 2381203e5..43aa204e8 100644 --- a/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java +++ b/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; diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 2e1e34a78..ac913d316 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/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 mountHandle = new AtomicReference<>(null); @Inject - Vault(VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty lastKnownException, VaultStats stats, WindowsDriveLetters windowsDriveLetters, Mounter mounter) { + Vault(VaultSettings vaultSettings, // + VaultConfigCache configCache, // + AtomicReference cryptoFileSystem, // + VaultState state, // + @Named("lastKnownException") ObjectProperty lastKnownException, // + VaultStats stats, // + Mounter mounter) { this.vaultSettings = vaultSettings; this.configCache = configCache; this.cryptoFileSystem = cryptoFileSystem; diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 46542ccb9..fd451d255 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/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"), // diff --git a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java index 653c4c6e6..8cb49a679 100644 --- a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java +++ b/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 selectedMountService; private final ResourceBundle resourceBundle; - private final BooleanExpression loopbackPortSupported; + private final ObservableValue loopbackPortSupported; private final ObservableValue mountToDirSupported; private final ObservableValue mountToDriveLetterSupported; private final ObservableValue mountFlagsSupported; private final ObservableValue readonlySupported; - private final ObservableValue fuseRestartRequired; private final Lazy application; private final List mountProviders; public ChoiceBox volumeTypeChoiceBox; @@ -46,7 +41,10 @@ public class VolumePreferencesController implements FxController { public Button loopbackPortApplyButton; @Inject - VolumePreferencesController(Settings settings, Lazy application, List mountProviders, @Named("FUPFMS") AtomicReference firstUsedProblematicFuseMountService, ResourceBundle resourceBundle) { + VolumePreferencesController(Settings settings, // + Lazy application, // + List 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 loopbackPortSupportedProperty() { return loopbackPortSupported; } public boolean isLoopbackPortSupported() { - return loopbackPortSupported.get(); + return loopbackPortSupported.getValue(); } public ObservableValue readonlySupportedProperty() { @@ -141,14 +133,6 @@ public class VolumePreferencesController implements FxController { return mountFlagsSupported.getValue(); } - public ObservableValue fuseRestartRequiredProperty() { - return fuseRestartRequired; - } - - public boolean getFuseRestartRequired() { - return fuseRestartRequired.getValue(); - } - /* Helpers */ private class MountServiceConverter extends StringConverter { diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockFuseRestartRequiredController.java b/src/main/java/org/cryptomator/ui/unlock/UnlockFuseRestartRequiredController.java new file mode 100644 index 000000000..21fe13733 --- /dev/null +++ b/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(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index f93999d21..9c3f84c6b 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +++ b/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); + } diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index 0d2183d23..9b5fa74b1 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/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 { private final VaultService vaultService; private final Lazy successScene; private final Lazy invalidMountPointScene; + private final Lazy fuseRestartRequiredScene; private final FxApplicationWindows appWindows; private final KeyLoadingStrategy keyLoadingStrategy; private final ObjectProperty illegalMountPointException; @Inject - UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, @UnlockWindow ObjectProperty illegalMountPointException) { + UnlockWorkflow(@UnlockWindow Stage window, // + @UnlockWindow Vault vault, // + VaultService vaultService, // + @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, // + @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, // + @FxmlScene(FxmlFile.UNLOCK_FUSE_RESTART_REQUIRED) Lazy fuseRestartRequiredScene, // + FxApplicationWindows appWindows, // + @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, // + @UnlockWindow ObjectProperty 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 { } } + 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 { 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()); diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java index 5eeab43e0..6340852f4 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java +++ b/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; private final ObservableValue defaultMountFlags; private final ObservableValue mountpointDirSupported; private final ObservableValue mountpointDriveLetterSupported; private final ObservableValue readOnlySupported; private final ObservableValue mountFlagsSupported; + private final ObservableValue defaultMountServiceSelected; private final ObservableValue directoryPath; private final FxApplicationWindows applicationWindows; + private final List mountProviders; + private final ObservableValue defaultMountService; + private final ObservableValue selectedMountService; + private final ObservableValue fuseRestartRequired; + private final ObservableValue loopbackPortChangeable; //-- FXML objects -- @@ -56,30 +74,62 @@ public class MountOptionsController implements FxController { public RadioButton mountPointDirBtn; public TextField directoryPathField; public ChoiceBox driveLetterSelection; + public ChoiceBox vaultVolumeTypeChoiceBox; + public TextField vaultLoopbackPortField; + public Button vaultLoopbackPortApplyButton; + @Inject - MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue mountService, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, FxApplicationWindows applicationWindows) { + MountOptionsController(@VaultOptionsWindow Stage window, // + @VaultOptionsWindow Vault vault, // + WindowsDriveLetters windowsDriveLetters, // + ResourceBundle resourceBundle, // + FxApplicationWindows applicationWindows, // + Lazy application, // + List mountProviders, // + @Named("FUPFMS") AtomicReference firstUsedProblematicFuseMountService, // + ObservableValue 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 defaultMountServiceSelectedProperty() { + return defaultMountServiceSelected; + } + + public boolean isDefaultMountServiceSelected() { + return defaultMountServiceSelected.getValue(); + } + public ObservableValue mountpointDirSupportedProperty() { return mountpointDirSupported; } @@ -274,4 +366,37 @@ public class MountOptionsController implements FxController { public String getDirectoryPath() { return directoryPath.getValue(); } + + public ObservableValue fuseRestartRequiredProperty() { + return fuseRestartRequired; + } + + public boolean getFuseRestartRequired() { + return fuseRestartRequired.getValue(); + } + + public ObservableValue loopbackPortChangeableProperty() { + return loopbackPortChangeable; + } + + public boolean isLoopbackPortChangeable() { + return loopbackPortChangeable.getValue(); + } + + private class MountServiceConverter extends StringConverter { + + @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(); + } + } } diff --git a/src/main/resources/fxml/preferences_volume.fxml b/src/main/resources/fxml/preferences_volume.fxml index f48b1c1c8..c173f03e2 100644 --- a/src/main/resources/fxml/preferences_volume.fxml +++ b/src/main/resources/fxml/preferences_volume.fxml @@ -32,8 +32,6 @@ -