diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml
index 8f0e2b968..8698d58ab 100644
--- a/.github/workflows/appimage.yml
+++ b/.github/workflows/appimage.yml
@@ -80,6 +80,8 @@ jobs:
--vendor "Skymatic GmbH"
--copyright "(C) 2016 - 2023 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
+ --java-options "--enable-preview"
+ --java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64"
--java-options "-Xss5m"
--java-options "-Xmx256m"
--java-options "-Dcryptomator.appVersion=\"${{ needs.get-version.outputs.semVerStr }}\""
diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml
index 1fbe44d06..55b1f325d 100644
--- a/.github/workflows/mac-dmg.yml
+++ b/.github/workflows/mac-dmg.yml
@@ -89,6 +89,8 @@ jobs:
--vendor "Skymatic GmbH"
--copyright "(C) 2016 - 2023 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}"
+ --java-options "--enable-preview"
+ --java-options "--enable-native-access=org.cryptomator.jfuse.mac"
--java-options "-Xss5m"
--java-options "-Xmx256m"
--java-options "-Dfile.encoding=\"utf-8\""
diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml
index ae95e4278..0a75c44ca 100644
--- a/.github/workflows/win-exe.yml
+++ b/.github/workflows/win-exe.yml
@@ -83,6 +83,8 @@ jobs:
--vendor "Skymatic GmbH"
--copyright "(C) 2016 - 2023 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
+ --java-options "--enable-preview"
+ --java-options "--enable-native-access=org.cryptomator.jfuse.win"
--java-options "-Xss5m"
--java-options "-Xmx256m"
--java-options "-Dcryptomator.appVersion=\"${{ needs.get-version.outputs.semVerStr }}\""
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 18e7c76e0..e9c70c10f 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -45,7 +45,7 @@
\ No newline at end of file
diff --git a/.idea/runConfigurations/Cryptomator_Linux.xml b/.idea/runConfigurations/Cryptomator_Linux.xml
index 6378f9a00..887a5044e 100644
--- a/.idea/runConfigurations/Cryptomator_Linux.xml
+++ b/.idea/runConfigurations/Cryptomator_Linux.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/.idea/runConfigurations/Cryptomator_Linux_Dev.xml b/.idea/runConfigurations/Cryptomator_Linux_Dev.xml
index 169d630b8..1bcd27ff3 100644
--- a/.idea/runConfigurations/Cryptomator_Linux_Dev.xml
+++ b/.idea/runConfigurations/Cryptomator_Linux_Dev.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/.idea/runConfigurations/Cryptomator_Windows.xml b/.idea/runConfigurations/Cryptomator_Windows.xml
index acf4da5f7..3c4ecc4a3 100644
--- a/.idea/runConfigurations/Cryptomator_Windows.xml
+++ b/.idea/runConfigurations/Cryptomator_Windows.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/.idea/runConfigurations/Cryptomator_Windows_Dev.xml b/.idea/runConfigurations/Cryptomator_Windows_Dev.xml
index 4bc869723..2093aee5e 100644
--- a/.idea/runConfigurations/Cryptomator_Windows_Dev.xml
+++ b/.idea/runConfigurations/Cryptomator_Windows_Dev.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/.idea/runConfigurations/Cryptomator_macOS.xml b/.idea/runConfigurations/Cryptomator_macOS.xml
index c5e8fd815..03e231c77 100644
--- a/.idea/runConfigurations/Cryptomator_macOS.xml
+++ b/.idea/runConfigurations/Cryptomator_macOS.xml
@@ -5,7 +5,7 @@
-
+
diff --git a/.idea/runConfigurations/Cryptomator_macOS_Dev.xml b/.idea/runConfigurations/Cryptomator_macOS_Dev.xml
index 01ee81f60..4aed7aed5 100644
--- a/.idea/runConfigurations/Cryptomator_macOS_Dev.xml
+++ b/.idea/runConfigurations/Cryptomator_macOS_Dev.xml
@@ -5,7 +5,7 @@
-
+
diff --git a/dist/linux/launcher-gtk2.properties b/dist/linux/launcher-gtk2.properties
index bf4727fa9..d778d6878 100644
--- a/dist/linux/launcher-gtk2.properties
+++ b/dist/linux/launcher-gtk2.properties
@@ -1,5 +1,7 @@
java-options=-Xss5m \
-Xmx256m \
+ --enable-preview \
+ --enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64 \
-Dfile.encoding=\"utf-8\" \
-Dcryptomator.appVersion=\"${SEMVER_STR}\" \
-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\" \
diff --git a/dist/linux/launcher.sh b/dist/linux/launcher.sh
index 1980edc13..9b9b513d4 100755
--- a/dist/linux/launcher.sh
+++ b/dist/linux/launcher.sh
@@ -10,4 +10,6 @@ java \
-Djdk.gtk.version=2 \
-Xss2m \
-Xmx512m \
+ --enable-preview \
+ --enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64 \
-m org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator
diff --git a/dist/mac/dmg/build.sh b/dist/mac/dmg/build.sh
index fa80db05b..75044bf86 100755
--- a/dist/mac/dmg/build.sh
+++ b/dist/mac/dmg/build.sh
@@ -65,6 +65,8 @@ ${JAVA_HOME}/bin/jpackage \
--vendor "${VENDOR}" \
--copyright "(C) ${COPYRIGHT_YEARS} ${VENDOR}" \
--app-version "${VERSION_NO}" \
+ --java-options "--enable-preview" \
+ --java-options "--enable-native-access=org.cryptomator.jfuse.mac" \
--java-options "-Xss5m" \
--java-options "-Xmx256m" \
--java-options "-Dfile.encoding=\"utf-8\"" \
diff --git a/dist/mac/launcher.sh b/dist/mac/launcher.sh
index d8cc30b45..eb12426a8 100755
--- a/dist/mac/launcher.sh
+++ b/dist/mac/launcher.sh
@@ -9,4 +9,6 @@ java \
-Dcryptomator.mountPointsDir="/Volumes" \
-Xss20m \
-Xmx512m \
+ --enable-preview \
+ --enable-native-access=org.cryptomator.jfuse.mac \
-m org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator
\ No newline at end of file
diff --git a/dist/win/build.ps1 b/dist/win/build.ps1
index 1e316d5f2..9973f5c12 100644
--- a/dist/win/build.ps1
+++ b/dist/win/build.ps1
@@ -75,6 +75,8 @@ if ($clean -and (Test-Path -Path $appPath)) {
--name $AppName `
--vendor $Vendor `
--copyright $copyright `
+ --java-options "--enable-preview" `
+ --java-options "--enable-native-access=org.cryptomator.jfuse.win" `
--java-options "-Xss5m" `
--java-options "-Xmx256m" `
--java-options "-Dcryptomator.appVersion=`"$semVerNo`"" `
diff --git a/dist/win/launcher.bat b/dist/win/launcher.bat
index 751a6e4fa..8ab971dbb 100644
--- a/dist/win/launcher.bat
+++ b/dist/win/launcher.bat
@@ -9,4 +9,6 @@ java ^
-Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" ^
-Xss20m ^
-Xmx512m ^
+ --enable-preview `
+ --enable-native-access=org.cryptomator.jfuse.win `
-m org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index e64299462..ae0afbdf2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,8 @@
19
- com.github.serceman,com.github.jnr,org.ow2.asm,net.java.dev.jna,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh
+
+ com.github.jnr,org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh2.1.1
@@ -33,9 +34,9 @@
1.2.0-beta11.2.0-beta21.2.0-beta1
- 1.3.4
- 1.3.3
- 1.2.8
+ 2.0.0-beta4
+ 2.0.0-beta2
+ 2.0.0-beta43.12.0
@@ -314,6 +315,7 @@
-Adagger.fastInit=enabled-Adagger.formatGeneratedSource=enabled
+ --enable-preview
diff --git a/src/main/java/org/cryptomator/common/CommonsModule.java b/src/main/java/org/cryptomator/common/CommonsModule.java
index 8d9155d8b..ddd782d35 100644
--- a/src/main/java/org/cryptomator/common/CommonsModule.java
+++ b/src/main/java/org/cryptomator/common/CommonsModule.java
@@ -10,19 +10,17 @@ 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;
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.frontend.webdav.WebDavServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Singleton;
-import javafx.beans.binding.Binding;
-import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import java.net.InetSocketAddress;
import java.security.NoSuchAlgorithmException;
@@ -34,7 +32,7 @@ import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
-@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class})
+@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class, MountModule.class})
public abstract class CommonsModule {
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
@@ -138,13 +136,4 @@ public abstract class CommonsModule {
});
}
- @Provides
- @Singleton
- static WebDavServer provideWebDavServer(ObservableValue serverSocketAddressBinding) {
- WebDavServer server = WebDavServer.create();
- // no need to unsubscribe eventually, because server is a singleton
- EasyBind.subscribe(serverSocketAddressBinding, server::bind);
- return server;
- }
-
}
diff --git a/src/main/java/org/cryptomator/common/ObservableUtil.java b/src/main/java/org/cryptomator/common/ObservableUtil.java
new file mode 100644
index 000000000..289f6e929
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/ObservableUtil.java
@@ -0,0 +1,18 @@
+package org.cryptomator.common;
+
+import javafx.beans.binding.Bindings;
+import javafx.beans.value.ObservableValue;
+import java.util.function.Function;
+
+public class ObservableUtil {
+
+ public static ObservableValue mapWithDefault(ObservableValue observable, Function super T, ? extends U> mapper, U defaultValue) {
+ return Bindings.createObjectBinding(() -> {
+ if (observable.getValue() == null) {
+ return defaultValue;
+ } else {
+ return mapper.apply(observable.getValue());
+ }
+ }, observable);
+ }
+}
diff --git a/src/main/java/org/cryptomator/common/mount/ActualMountService.java b/src/main/java/org/cryptomator/common/mount/ActualMountService.java
new file mode 100644
index 000000000..a96cc8e37
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/ActualMountService.java
@@ -0,0 +1,6 @@
+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/IllegalMountPointException.java b/src/main/java/org/cryptomator/common/mount/IllegalMountPointException.java
new file mode 100644
index 000000000..5fdb1d91c
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/IllegalMountPointException.java
@@ -0,0 +1,9 @@
+package org.cryptomator.common.mount;
+
+public class IllegalMountPointException extends IllegalArgumentException {
+
+ public IllegalMountPointException(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
new file mode 100644
index 000000000..d78cc3216
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountModule.java
@@ -0,0 +1,51 @@
+package org.cryptomator.common.mount;
+
+import dagger.Module;
+import dagger.Provides;
+import org.cryptomator.common.settings.Settings;
+import org.cryptomator.integrations.mount.MountService;
+
+import javax.inject.Singleton;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableValue;
+import java.util.List;
+
+@Module
+public class MountModule {
+
+ @Provides
+ @Singleton
+ static List provideSupportedMountServices() {
+ return MountService.get().toList();
+ }
+
+ //currently not used, because macFUSE and FUSE-T cannot be used in the same JVM
+ /*
+ @Provides
+ @Singleton
+ static ObservableValue provideMountService(Settings settings, List serviceImpls) {
+ var fallbackProvider = serviceImpls.stream().findFirst().orElse(null);
+ return ObservableUtil.mapWithDefault(settings.mountService(), //
+ desiredServiceImpl -> { //
+ var desiredService = serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(desiredServiceImpl)).findAny(); //
+ return new ActualMountService(desiredService.orElse(fallbackProvider), desiredService.isPresent()); //
+ }, //
+ new ActualMountService(fallbackProvider, true));
+ }
+ */
+
+ @Provides
+ @Singleton
+ static ActualMountService provideActualMountService(Settings settings, List serviceImpls) {
+ var fallbackProvider = serviceImpls.stream().findFirst().orElse(null);
+ var desiredService = serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(settings.mountService().getValue())).findFirst(); //
+ return new ActualMountService(desiredService.orElse(fallbackProvider), desiredService.isPresent()); //
+ }
+
+ @Provides
+ @Singleton
+ static ObservableValue provideMountService(ActualMountService service) {
+ return new SimpleObjectProperty<>(service);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointNotExistsException.java b/src/main/java/org/cryptomator/common/mount/MountPointNotExistsException.java
new file mode 100644
index 000000000..e90523bc2
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountPointNotExistsException.java
@@ -0,0 +1,8 @@
+package org.cryptomator.common.mount;
+
+public class MountPointNotExistsException extends IllegalMountPointException {
+
+ public MountPointNotExistsException(String msg) {
+ super(msg);
+ }
+}
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointNotSupportedException.java b/src/main/java/org/cryptomator/common/mount/MountPointNotSupportedException.java
new file mode 100644
index 000000000..e321f23b1
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountPointNotSupportedException.java
@@ -0,0 +1,8 @@
+package org.cryptomator.common.mount;
+
+public class MountPointNotSupportedException extends IllegalMountPointException {
+
+ public MountPointNotSupportedException(String msg) {
+ super(msg);
+ }
+}
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java b/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java
new file mode 100644
index 000000000..fb481167c
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java
@@ -0,0 +1,8 @@
+package org.cryptomator.common.mount;
+
+public class MountPointPreparationException extends RuntimeException {
+
+ public MountPointPreparationException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java b/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java
new file mode 100644
index 000000000..ca82b54da
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java
@@ -0,0 +1,110 @@
+package org.cryptomator.common.mount;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+
+public final class MountWithinParentUtil {
+
+ private static final Logger LOG = LoggerFactory.getLogger(Mounter.class);
+ private static final String HIDEAWAY_PREFIX = ".~$";
+ private static final String HIDEAWAY_SUFFIX = ".tmp";
+ private static final String WIN_HIDDEN_ATTR = "dos:hidden";
+
+ private MountWithinParentUtil() {}
+
+ static void prepareParentNoMountPoint(Path mountPoint) throws MountPointPreparationException {
+ Path hideaway = getHideaway(mountPoint);
+ var mpExists = Files.exists(mountPoint, LinkOption.NOFOLLOW_LINKS);
+ var hideExists = Files.exists(hideaway, LinkOption.NOFOLLOW_LINKS);
+
+ //TODO: possible improvement by just deleting an _empty_ hideaway
+ if (mpExists && hideExists) { //both resources exist (whatever type)
+ throw new MountPointPreparationException(new FileAlreadyExistsException(hideaway.toString()));
+ } else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist
+ throw new MountPointPreparationException(new NoSuchFileException(mountPoint.toString()));
+ } else if (!mpExists) { //only hideaway exists
+ checkIsDirectory(hideaway);
+ LOG.info("Mountpoint {} seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint);
+ try {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ Files.setAttribute(hideaway, WIN_HIDDEN_ATTR, true, LinkOption.NOFOLLOW_LINKS);
+ }
+ } catch (IOException e) {
+ throw new MountPointPreparationException(e);
+ }
+ } else { //only mountpoint exists
+ try {
+ checkIsDirectory(mountPoint);
+ checkIsEmpty(mountPoint);
+
+ Files.move(mountPoint, hideaway);
+ if (SystemUtils.IS_OS_WINDOWS) {
+ Files.setAttribute(hideaway, WIN_HIDDEN_ATTR, true, LinkOption.NOFOLLOW_LINKS);
+ }
+ } catch (IOException e) {
+ throw new MountPointPreparationException(e);
+ }
+ }
+ }
+
+ static void cleanup(Path mountPoint) {
+ Path hideaway = getHideaway(mountPoint);
+ try {
+ waitForMountpointRestoration(mountPoint);
+ Files.move(hideaway, mountPoint);
+ if (SystemUtils.IS_OS_WINDOWS) {
+ Files.setAttribute(mountPoint, WIN_HIDDEN_ATTR, false);
+ }
+ } catch (IOException e) {
+ LOG.error("Unable to restore hidden directory to mountpoint {}.", mountPoint, e);
+ }
+ }
+
+ //on Windows removing the mountpoint takes some time, so we poll for at most 3 seconds
+ private static void waitForMountpointRestoration(Path mountPoint) throws FileAlreadyExistsException {
+ int attempts = 0;
+ while (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
+ attempts++;
+ if (attempts >= 5) {
+ throw new FileAlreadyExistsException("Timeout waiting for mountpoint cleanup for " + mountPoint + " .");
+ }
+
+ try {
+ Thread.sleep(300);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new FileAlreadyExistsException("Interrupted before mountpoint " + mountPoint + " was cleared");
+ }
+ }
+ }
+
+ private static void checkIsDirectory(Path toCheck) throws MountPointPreparationException {
+ if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) {
+ throw new MountPointPreparationException(new NotDirectoryException(toCheck.toString()));
+ }
+ }
+
+ private static void checkIsEmpty(Path toCheck) throws MountPointPreparationException, IOException {
+ try (var dirStream = Files.list(toCheck)) {
+ if (dirStream.findFirst().isPresent()) {
+ throw new MountPointPreparationException(new DirectoryNotEmptyException(toCheck.toString()));
+ }
+ }
+ }
+
+ //visible for testing
+ static Path getHideaway(Path mountPoint) {
+ return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/common/mount/Mounter.java b/src/main/java/org/cryptomator/common/mount/Mounter.java
new file mode 100644
index 000000000..2c75d3f89
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/Mounter.java
@@ -0,0 +1,138 @@
+package org.cryptomator.common.mount;
+
+import org.cryptomator.common.Environment;
+import org.cryptomator.common.settings.Settings;
+import org.cryptomator.common.settings.VaultSettings;
+import org.cryptomator.integrations.mount.Mount;
+import org.cryptomator.integrations.mount.MountBuilder;
+import org.cryptomator.integrations.mount.MountFailedException;
+import org.cryptomator.integrations.mount.MountService;
+
+import javax.inject.Inject;
+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.Objects;
+
+import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER;
+import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR;
+import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_SYSTEM_CHOSEN_PATH;
+import static org.cryptomator.integrations.mount.MountCapability.MOUNT_WITHIN_EXISTING_PARENT;
+import static org.cryptomator.integrations.mount.MountCapability.UNMOUNT_FORCED;
+
+@Singleton
+public class Mounter {
+
+ private final Settings settings;
+ private final Environment env;
+ private final WindowsDriveLetters driveLetters;
+ private final ObservableValue mountServiceObservable;
+
+ @Inject
+ public Mounter(Settings settings, Environment env, WindowsDriveLetters driveLetters, ObservableValue mountServiceObservable) {
+ this.settings = settings;
+ this.env = env;
+ this.driveLetters = driveLetters;
+ this.mountServiceObservable = mountServiceObservable;
+ }
+
+ private class SettledMounter {
+
+ private MountService service;
+ private MountBuilder builder;
+ private VaultSettings vaultSettings;
+
+ public SettledMounter(MountService service, MountBuilder builder, VaultSettings vaultSettings) {
+ this.service = service;
+ this.builder = builder;
+ this.vaultSettings = vaultSettings;
+ }
+
+ Runnable prepare() throws IOException {
+ 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_HOST_NAME -> env.getLoopbackAlias().ifPresent(builder::setLoopbackHostName);
+ case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode().get());
+ case MOUNT_FLAGS -> {
+ var mountFlags = vaultSettings.mountFlags().get();
+ if( mountFlags == null || mountFlags.isBlank()) {
+ builder.setMountFlags(service.getDefaultMountFlags());
+ } else {
+ builder.setMountFlags(mountFlags);
+ }
+ }
+ case VOLUME_ID -> builder.setVolumeId(vaultSettings.getId());
+ case VOLUME_NAME -> builder.setVolumeName(vaultSettings.mountName().get());
+ }
+ }
+
+ return prepareMountPoint();
+ }
+
+ private Runnable prepareMountPoint() throws IOException {
+ Runnable cleanup = () -> {};
+ var userChosenMountPoint = vaultSettings.getMountPoint();
+ var defaultMountPointBase = env.getMountPointsDir().orElseThrow();
+ var canMountToDriveLetter = service.hasCapability(MOUNT_AS_DRIVE_LETTER);
+ var canMountToParent = service.hasCapability(MOUNT_WITHIN_EXISTING_PARENT);
+ var canMountToDir = service.hasCapability(MOUNT_TO_EXISTING_DIR);
+
+ if (userChosenMountPoint == null) {
+ if (service.hasCapability(MOUNT_TO_SYSTEM_CHOSEN_PATH)) {
+ // no need to set a mount point
+ } else if (canMountToDriveLetter) {
+ builder.setMountpoint(driveLetters.getFirstDesiredAvailable().orElseThrow()); //TODO: catch exception and translate
+ } else if (canMountToParent) {
+ Files.createDirectories(defaultMountPointBase);
+ builder.setMountpoint(defaultMountPointBase);
+ } else if (canMountToDir) {
+ var mountPoint = defaultMountPointBase.resolve(vaultSettings.mountName().get());
+ Files.createDirectories(mountPoint);
+ builder.setMountpoint(mountPoint);
+ }
+ } else {
+ if (canMountToParent && !canMountToDir) {
+ MountWithinParentUtil.prepareParentNoMountPoint(userChosenMountPoint);
+ cleanup = () -> {
+ MountWithinParentUtil.cleanup(userChosenMountPoint);
+ };
+ }
+ try {
+ builder.setMountpoint(userChosenMountPoint);
+ } catch (IllegalArgumentException e) {
+ var mpIsDriveLetter = userChosenMountPoint.toString().matches("[A-Z]:\\\\");
+ var configNotSupported = (!canMountToDriveLetter && mpIsDriveLetter) || (!canMountToDir && !mpIsDriveLetter) || (!canMountToParent && !mpIsDriveLetter);
+ if (configNotSupported) {
+ throw new MountPointNotSupportedException(e.getMessage());
+ } else if (canMountToDir && !canMountToParent && !Files.exists(userChosenMountPoint)) {
+ //mountpoint must exist
+ throw new MountPointNotExistsException(e.getMessage());
+ } else {
+ //TODO: add specific exception for !canMountToDir && canMountToParent && !Files.notExists(userChosenMountPoint)
+ throw new IllegalMountPointException(e.getMessage());
+ }
+ }
+ }
+ return cleanup;
+ }
+
+ }
+
+ 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 cleanup = internal.prepare();
+ return new MountHandle(builder.mount(), mountService.hasCapability(UNMOUNT_FORCED), cleanup);
+ }
+
+ public record MountHandle(Mount mountObj, boolean supportsUnmountForced, Runnable specialCleanup) {
+
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/common/mount/WindowsDriveLetters.java b/src/main/java/org/cryptomator/common/mount/WindowsDriveLetters.java
new file mode 100644
index 000000000..ad65ff8b9
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/WindowsDriveLetters.java
@@ -0,0 +1,70 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the accompanying LICENSE file.
+ *******************************************************************************/
+package org.cryptomator.common.mount;
+
+import com.google.common.collect.Sets;
+import org.apache.commons.lang3.SystemUtils;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.StreamSupport;
+
+@Singleton
+public final class WindowsDriveLetters {
+
+ private static final Set A_TO_Z;
+
+ static {
+ var sortedSet = new TreeSet();
+ IntStream.rangeClosed('A', 'Z').mapToObj(i -> Path.of((char) i + ":\\")).forEach(sortedSet::add);
+ A_TO_Z = Collections.unmodifiableSet(sortedSet);
+ }
+
+ @Inject
+ public WindowsDriveLetters() {
+ }
+
+ public Set getAll() {
+ return A_TO_Z;
+ }
+
+ public Set getOccupied() {
+ if (!SystemUtils.IS_OS_WINDOWS) {
+ return Set.of();
+ } else {
+ Iterable rootDirs = FileSystems.getDefault().getRootDirectories();
+ return StreamSupport.stream(rootDirs.spliterator(), false).collect(Collectors.toUnmodifiableSet());
+ }
+ }
+
+ public Set getAvailable() {
+ return Sets.difference(getAll(), getOccupied());
+ }
+
+ /**
+ * Skips A and B and only returns them if all others are occupied.
+ *
+ * @return an Optional containing either the letter of a free drive letter or empty, if none is available
+ */
+ public Optional getFirstDesiredAvailable() {
+ var availableDriveLetters = getAvailable();
+ var optString = availableDriveLetters.stream().filter(this::notAOrB).findFirst();
+ return optString.or(() -> availableDriveLetters.stream().findFirst());
+ }
+
+ private boolean notAOrB(Path driveLetter) {
+ return !(Path.of("A:\\").equals(driveLetter) || Path.of("B:\\").equals(driveLetter));
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/common/mountpoint/AvailableDriveLetterChooser.java b/src/main/java/org/cryptomator/common/mountpoint/AvailableDriveLetterChooser.java
deleted file mode 100644
index 23ce8466a..000000000
--- a/src/main/java/org/cryptomator/common/mountpoint/AvailableDriveLetterChooser.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.cryptomator.common.mountpoint;
-
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.vaults.Volume;
-import org.cryptomator.common.vaults.WindowsDriveLetters;
-
-import javax.inject.Inject;
-import java.nio.file.Path;
-import java.util.Optional;
-
-class AvailableDriveLetterChooser implements MountPointChooser {
-
- private final WindowsDriveLetters windowsDriveLetters;
-
- @Inject
- public AvailableDriveLetterChooser(WindowsDriveLetters windowsDriveLetters) {
- this.windowsDriveLetters = windowsDriveLetters;
- }
-
- @Override
- public boolean isApplicable(Volume caller) {
- return SystemUtils.IS_OS_WINDOWS;
- }
-
- @Override
- public Optional chooseMountPoint(Volume caller) {
- return this.windowsDriveLetters.getDesiredAvailableDriveLetterPath();
- }
-}
diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomDriveLetterChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomDriveLetterChooser.java
deleted file mode 100644
index 02f75d4a1..000000000
--- a/src/main/java/org/cryptomator/common/mountpoint/CustomDriveLetterChooser.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.cryptomator.common.mountpoint;
-
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.settings.VaultSettings;
-import org.cryptomator.common.vaults.Volume;
-
-import javax.inject.Inject;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Optional;
-
-class CustomDriveLetterChooser implements MountPointChooser {
-
- private final VaultSettings vaultSettings;
-
- @Inject
- public CustomDriveLetterChooser(VaultSettings vaultSettings) {
- this.vaultSettings = vaultSettings;
- }
-
- @Override
- public boolean isApplicable(Volume caller) {
- return SystemUtils.IS_OS_WINDOWS;
- }
-
- @Override
- public Optional chooseMountPoint(Volume caller) {
- return this.vaultSettings.getWinDriveLetter().map(letter -> letter.charAt(0) + ":\\").map(Paths::get);
- }
-
- @Override
- public boolean prepare(Volume caller, Path driveLetter) throws InvalidMountPointException {
- if (!Files.notExists(driveLetter, LinkOption.NOFOLLOW_LINKS)) {
- //Drive already exists OR can't be determined
- throw new InvalidMountPointException(new FileAlreadyExistsException(driveLetter.toString()));
- }
- return false;
- }
-}
diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java
deleted file mode 100644
index 295e922c1..000000000
--- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java
+++ /dev/null
@@ -1,167 +0,0 @@
-package org.cryptomator.common.mountpoint;
-
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.settings.VaultSettings;
-import org.cryptomator.common.settings.VolumeImpl;
-import org.cryptomator.common.vaults.MountPointRequirement;
-import org.cryptomator.common.vaults.Volume;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import java.io.IOException;
-import java.nio.file.DirectoryNotEmptyException;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.NotDirectoryException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Optional;
-
-class CustomMountPointChooser implements MountPointChooser {
-
- private static final String HIDEAWAY_PREFIX = ".~$";
- private static final String HIDEAWAY_SUFFIX = ".tmp";
- private static final String WIN_HIDDEN = "dos:hidden";
- private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class);
-
- private final VaultSettings vaultSettings;
-
- @Inject
- public CustomMountPointChooser(VaultSettings vaultSettings) {
- this.vaultSettings = vaultSettings;
- }
-
- @Override
- public boolean isApplicable(Volume caller) {
- return caller.getImplementationType() != VolumeImpl.WEBDAV;
- }
-
- @Override
- public Optional chooseMountPoint(Volume caller) {
- //VaultSettings#getCustomMountPath already checks whether the saved custom mountpoint should be used
- return this.vaultSettings.getCustomMountPath().map(Paths::get);
- }
-
- @Override
- public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
- return switch (caller.getMountPointRequirement()) {
- case PARENT_NO_MOUNT_POINT -> {
- prepareParentNoMountPoint(mountPoint);
- LOG.debug("Successfully checked custom mount point: {}", mountPoint);
- yield true;
- }
- case EMPTY_MOUNT_POINT -> {
- prepareEmptyMountPoint(mountPoint);
- LOG.debug("Successfully checked custom mount point: {}", mountPoint);
- yield false;
- }
- case NONE, UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT -> {
- throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
- }
- };
- }
-
- //This is case on Windows when using FUSE
- //See https://github.com/billziss-gh/winfsp/issues/320
- void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException {
- Path hideaway = getHideaway(mountPoint);
- var mpExists = Files.exists(mountPoint, LinkOption.NOFOLLOW_LINKS);
- var hideExists = Files.exists(hideaway, LinkOption.NOFOLLOW_LINKS);
-
- //TODO: possible improvement by just deleting an _empty_ hideaway
- if (mpExists && hideExists) { //both resources exist (whatever type)
- throw new InvalidMountPointException(new FileAlreadyExistsException(hideaway.toString()));
- } else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist
- throw new InvalidMountPointException(new NoSuchFileException(mountPoint.toString()));
- } else if (!mpExists) { //only hideaway exists
- checkIsDirectory(hideaway);
- LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint);
- try {
- if (SystemUtils.IS_OS_WINDOWS) {
- Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS);
- }
- } catch (IOException e) {
- throw new InvalidMountPointException(e);
- }
- } else { //only mountpoint exists
- try {
- checkIsDirectory(mountPoint);
- checkIsEmpty(mountPoint);
-
- Files.move(mountPoint, hideaway);
- if (SystemUtils.IS_OS_WINDOWS) {
- Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS);
- }
- } catch (IOException e) {
- throw new InvalidMountPointException(e);
- }
- }
- }
-
- private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException {
- //This is the case for Windows when using Dokany and for Linux and Mac
- checkIsDirectory(mountPoint);
- try {
- checkIsEmpty(mountPoint);
- } catch (IOException exception) {
- throw new InvalidMountPointException("IOException while checking folder content", exception);
- }
- }
-
- @Override
- public void cleanup(Volume caller, Path mountPoint) {
- if (caller.getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT) {
- Path hideaway = getHideaway(mountPoint);
- try {
- waitForMountpointRestoration(mountPoint);
- Files.move(hideaway, mountPoint);
- if (SystemUtils.IS_OS_WINDOWS) {
- Files.setAttribute(mountPoint, WIN_HIDDEN, false);
- }
- } catch (IOException e) {
- LOG.error("Unable to clean up mountpoint {} for Winfsp mounting.", mountPoint, e);
- }
- }
- }
-
- //on Windows removing the mountpoint takes some time, so we poll for at most 3 seconds
- private void waitForMountpointRestoration(Path mountPoint) throws FileAlreadyExistsException {
- int attempts = 0;
- while (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
- attempts++;
- if (attempts >= 10) {
- throw new FileAlreadyExistsException("Timeout waiting for mountpoint cleanup for " + mountPoint + " .");
- }
-
- try {
- Thread.sleep(300);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new FileAlreadyExistsException("Interrupted before mountpoint " + mountPoint + " was cleared");
- }
- }
- }
-
- private void checkIsDirectory(Path toCheck) throws InvalidMountPointException {
- if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) {
- throw new InvalidMountPointException(new NotDirectoryException(toCheck.toString()));
- }
- }
-
- private void checkIsEmpty(Path toCheck) throws InvalidMountPointException, IOException {
- try (var dirStream = Files.list(toCheck)) {
- if (dirStream.findFirst().isPresent()) {
- throw new InvalidMountPointException(new DirectoryNotEmptyException(toCheck.toString()));
- }
- }
- }
-
- //visible for testing
- Path getHideaway(Path mountPoint) {
- return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX);
- }
-
-}
diff --git a/src/main/java/org/cryptomator/common/mountpoint/InvalidMountPointException.java b/src/main/java/org/cryptomator/common/mountpoint/InvalidMountPointException.java
deleted file mode 100644
index b0c2fbd7a..000000000
--- a/src/main/java/org/cryptomator/common/mountpoint/InvalidMountPointException.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.cryptomator.common.mountpoint;
-
-public class InvalidMountPointException extends Exception {
-
- public InvalidMountPointException(String message) {
- super(message);
- }
-
- public InvalidMountPointException(Throwable cause) {
- super(cause);
- }
-
- public InvalidMountPointException(String message, Throwable cause) {
- super(message, cause);
- }
-}
diff --git a/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java b/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java
deleted file mode 100644
index 334746860..000000000
--- a/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.cryptomator.common.mountpoint;
-
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.settings.VaultSettings;
-import org.cryptomator.common.vaults.Volume;
-
-import javax.inject.Inject;
-import java.nio.file.Path;
-import java.util.Optional;
-
-class MacVolumeMountChooser implements MountPointChooser {
-
- private static final Path VOLUME_PATH = Path.of("/Volumes");
-
- private final VaultSettings vaultSettings;
- private final MountPointHelper helper;
-
- @Inject
- public MacVolumeMountChooser(VaultSettings vaultSettings, MountPointHelper helper) {
- this.vaultSettings = vaultSettings;
- this.helper = helper;
- }
-
- @Override
- public boolean isApplicable(Volume caller) {
- return SystemUtils.IS_OS_MAC;
- }
-
- @Override
- public Optional chooseMountPoint(Volume caller) {
- return Optional.of(helper.chooseTemporaryMountPoint(vaultSettings, VOLUME_PATH));
- }
-
- @Override
- public boolean prepare(Volume caller, Path mountPoint) {
- // https://github.com/osxfuse/osxfuse/issues/306#issuecomment-245114592:
- // In order to allow non-admin users to mount FUSE volumes in `/Volumes`,
- // starting with version 3.5.0, FUSE will create non-existent mount points automatically.
- // Therefore we don't need to prepare anything.
- return false;
- }
-}
diff --git a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java
deleted file mode 100644
index 1e36ba5f6..000000000
--- a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java
+++ /dev/null
@@ -1,138 +0,0 @@
-package org.cryptomator.common.mountpoint;
-
-import dagger.multibindings.IntKey;
-import org.cryptomator.common.vaults.Volume;
-
-import java.nio.file.Path;
-import java.util.Optional;
-import java.util.SortedSet;
-
-/**
- * Base interface for the Mountpoint-Choosing-Operation that results in the choice and
- * preparation of a mountpoint or an exception otherwise.
- *
All MountPointChoosers (MPCs) need to implement this class and must be added to
- * the pool of possible MPCs by the {@link MountPointChooserModule MountPointChooserModule.}
- * The MountPointChooserModule will sort them according to their {@link IntKey IntKey priority.}
- * The priority must be defined by the developer to reflect a useful execution order.
- * A specific priority must not be assigned to more than one MPC at a time;
- * the result of having two MPCs with equal priority is undefined.
- *
- *
MPCs are executed by a {@link Volume} in descending order of their priority
- * (higher priorities are tried first) to find and prepare a suitable mountpoint for the volume.
- * The volume has access to a {@link SortedSet} of MPCs in this specific order,
- * that is provided by the Module. The Set contains all available Choosers, even if they
- * are not {@link #isApplicable(Volume) applicable} for the Vault/Volume. The Volume must
- * check whether a MPC is applicable by invoking {@code #isApplicable(Volume)} on it
- * before calling {@code #chooseMountPoint(Volume)}.
- *
- *
At execution of a MPC {@link #chooseMountPoint(Volume)} is called to choose a mountpoint
- * according to the MPC's strategy. The strategy can involve reading configs,
- * searching the filesystem, processing user-input or similar operations.
- * If {@code #chooseMountPoint(Volume)} returns a non-null path (everything but
- * {@linkplain Optional#empty()}) the MPC's {@link #prepare(Volume, Path)} method is called and the
- * MountPoint is verified and/or prepared. In this case no other MPC's will be called for
- * this volume, even if {@code #prepare(Volume, Path)} fails.
- *
- *
If {@code #chooseMountPoint(Volume)} yields no result, the next MPC is executed
- * without first calling the {@code #prepare(Volume, Path)} method of the current MPC.
- * This is repeated until
- *
- *
either a mountpoint is returned by {@code #chooseMountPoint(Volume)}
- * and {@code #prepare(Volume, Path)} succeeds or fails, ending the entire operation
- *
or no MPC remains and an {@link InvalidMountPointException} is thrown.
- *
- * If the {@code #prepare(Volume, Path)} method of a MPC fails, the entire
- * Mountpoint-Choosing-Operation is aborted and the method should do all necessary cleanup
- * before throwing the exception.
- * If the preparation succeeds {@link #cleanup(Volume, Path)} can be used after unmount to do any
- * remaining cleanup.
- */
-public interface MountPointChooser {
-
- /**
- * Called by the {@link Volume} to determine whether this MountPointChooser is
- * applicable for mounting the Vault/Volume, especially with regard to the
- * current system configuration and particularities of the Volume type.
- *
- *
Developers should override this method to check for system configurations
- * that are unsuitable for this MPC.
- *
- * @param caller The Volume that is calling the method to determine applicability of the MPC
- * @return a boolean flag; true if applicable, else false.
- * @see #chooseMountPoint(Volume)
- */
- boolean isApplicable(Volume caller);
-
- /**
- * Called by a {@link Volume} to choose a mountpoint according to the
- * MountPointChoosers strategy.
- *
- *
This method must only be called for MPCs that were deemed
- * {@link #isApplicable(Volume) applicable} by the {@link Volume Volume.}
- * Developers should override this method to find or extract a mountpoint for
- * the volume without preparing it. Preparation should be done by
- * {@link #prepare(Volume, Path)} instead.
- * Exceptions in this method should be handled gracefully and result in returning
- * {@link Optional#empty()} instead of throwing an exception.
- *
- * @param caller The Volume that is calling the method to choose a mountpoint
- * @return the chosen path or {@link Optional#empty()} if an exception occurred
- * or no mountpoint could be found.
- * @see #isApplicable(Volume)
- * @see #prepare(Volume, Path)
- */
- Optional chooseMountPoint(Volume caller);
-
- /**
- * Called by a {@link Volume} to prepare and/or verify the chosen mountpoint.
- * This method is only called if the {@link #chooseMountPoint(Volume)} method
- * of the same MountPointChooser returned a path.
- *
- *
Developers should override this method to prepare the mountpoint for
- * the volume and check for any obstacles that could hinder the mount operation.
- * The mountpoint is deemed "prepared" if it can be used to mount a volume
- * without any further filesystem actions or user interaction. If this is not possible,
- * this method should fail. In other words: This method should not return without
- * either failing or finalizing the preparation of the mountpoint.
- * Generally speaking exceptions should be wrapped as
- * {@link InvalidMountPointException} to allow efficient handling by the caller.
- *
- *
Often the preparation of a mountpoint involves creating files or others
- * actions that require cleaning up after the volume is unmounted.
- * In this case developers should override the {@link #cleanup(Volume, Path)}
- * method and return {@code true} to the volume to indicate that the
- * {@code #cleanup} method of this MPC should be called after unmount.
- *
- *
Please note: If this method fails the entire
- * Mountpoint-Choosing-Operation is aborted without calling
- * {@link #cleanup(Volume, Path)} or any other MPCs. Therefore this method should
- * do all necessary cleanup before throwing the exception.
- *
- * @param caller The Volume that is calling the method to prepare a mountpoint
- * @param mountPoint the mountpoint chosen by {@link #chooseMountPoint(Volume)}
- * @return a boolean flag; true if cleanup is needed, false otherwise
- * @throws InvalidMountPointException if the preparation fails
- * @see #chooseMountPoint(Volume)
- * @see #cleanup(Volume, Path)
- */
- default boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
- return false; //NO-OP
- }
-
- /**
- * Called by a {@link Volume} to do any cleanup needed after unmount.
- *
- *
This method is only called if the {@link #prepare(Volume, Path)} method
- * of the same MountPointChooser returned {@code true}. Typically developers want to
- * delete any files created prior to mount or do similar tasks.
- * Exceptions in this method should be handled gracefully.
- *
- * @param caller The Volume that is calling the method to cleanup the prepared mountpoint
- * @param mountPoint the mountpoint that was prepared by {@link #prepare(Volume, Path)}
- * @see #prepare(Volume, Path)
- */
- default void cleanup(Volume caller, Path mountPoint) {
- //NO-OP
- }
-
-}
diff --git a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java b/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java
deleted file mode 100644
index 9c7893e42..000000000
--- a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package org.cryptomator.common.mountpoint;
-
-import com.google.common.collect.Iterables;
-import dagger.Binds;
-import dagger.Module;
-import dagger.Provides;
-import dagger.multibindings.IntKey;
-import dagger.multibindings.IntoMap;
-import org.cryptomator.common.vaults.PerVault;
-
-import javax.inject.Named;
-import java.util.Comparator;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-/**
- * Dagger-Module for {@link MountPointChooser MountPointChoosers.}
- * See there for additional information.
- *
- * @see MountPointChooser
- */
-@Module
-public abstract class MountPointChooserModule {
-
- @Binds
- @IntoMap
- @IntKey(1000)
- @PerVault
- public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser);
-
- @Binds
- @IntoMap
- @IntKey(900)
- @PerVault
- public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser);
-
- @Binds
- @IntoMap
- @IntKey(800)
- @PerVault
- public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);
-
- @Binds
- @IntoMap
- @IntKey(101)
- @PerVault
- public abstract MountPointChooser bindMacVolumeMountChooser(MacVolumeMountChooser chooser);
-
- @Binds
- @IntoMap
- @IntKey(100)
- @PerVault
- public abstract MountPointChooser bindTemporaryMountPointChooser(TemporaryMountPointChooser chooser);
-
- @Provides
- @PerVault
- @Named("orderedMountPointChoosers")
- public static Iterable provideOrderedMountPointChoosers(Map choosers) {
- SortedMap sortedChoosers = new TreeMap<>(Comparator.reverseOrder());
- sortedChoosers.putAll(choosers);
- return Iterables.unmodifiableIterable(sortedChoosers.values());
- }
-}
diff --git a/src/main/java/org/cryptomator/common/mountpoint/MountPointHelper.java b/src/main/java/org/cryptomator/common/mountpoint/MountPointHelper.java
deleted file mode 100644
index fe64902bd..000000000
--- a/src/main/java/org/cryptomator/common/mountpoint/MountPointHelper.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package org.cryptomator.common.mountpoint;
-
-import org.cryptomator.common.Environment;
-import org.cryptomator.common.settings.VaultSettings;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.DirectoryNotEmptyException;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.util.Optional;
-
-@Singleton
-class MountPointHelper {
-
- public static Logger LOG = LoggerFactory.getLogger(MountPointHelper.class);
- private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
-
- private final Optional tmpMountPointDir;
- private volatile boolean unmountDebrisCleared = false;
-
- @Inject
- public MountPointHelper(Environment env) {
- this.tmpMountPointDir = env.getMountPointsDir();
- }
-
- public Path chooseTemporaryMountPoint(VaultSettings vaultSettings, Path parentDir) {
- String basename = vaultSettings.mountName().get();
- //regular
- Path mountPoint = parentDir.resolve(basename);
- if (Files.notExists(mountPoint)) {
- return mountPoint;
- }
- //with id
- mountPoint = parentDir.resolve(basename + " (" + vaultSettings.getId() + ")");
- if (Files.notExists(mountPoint)) {
- return mountPoint;
- }
- //with id and count
- for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
- mountPoint = parentDir.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
- if (Files.notExists(mountPoint)) {
- return mountPoint;
- }
- }
- LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parentDir, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
- return null;
- }
-
- public synchronized void clearIrregularUnmountDebrisIfNeeded() {
- if (unmountDebrisCleared || tmpMountPointDir.isEmpty()) {
- return; // nothing to do
- }
- if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) {
- clearIrregularUnmountDebris(tmpMountPointDir.get());
- }
- unmountDebrisCleared = true;
- }
-
- private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {
- IOException cleanupFailed = new IOException("Cleanup failed");
-
- try (var ds = Files.newDirectoryStream(dirContainingMountPoints)) {
- LOG.debug("Performing cleanup of mountpoint dir {}.", dirContainingMountPoints);
- for (Path p : ds) {
- try {
- var attr = Files.readAttributes(p, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
- if (attr.isOther() && attr.isDirectory()) { // yes, this is possible with windows junction points -.-
- Files.delete(p);
- } else if (attr.isDirectory()) {
- deleteEmptyDir(p);
- } else if (attr.isSymbolicLink()) {
- deleteDeadLink(p);
- } else {
- LOG.debug("Found non-directory element in mountpoint dir: {}", p);
- }
- } catch (IOException e) {
- cleanupFailed.addSuppressed(e);
- }
- }
-
- if (cleanupFailed.getSuppressed().length > 0) {
- throw cleanupFailed;
- }
- } catch (IOException e) {
- LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e);
- } finally {
- unmountDebrisCleared = true;
- }
- }
-
- private void deleteEmptyDir(Path dir) throws IOException {
- assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
- try {
- ensureIsEmpty(dir);
- Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents)
- } catch (DirectoryNotEmptyException e) {
- LOG.info("Found non-empty directory in mountpoint dir: {}", dir);
- }
- }
-
- private void deleteDeadLink(Path symlink) throws IOException {
- assert Files.isSymbolicLink(symlink);
- if (Files.notExists(symlink)) { // following link: target does not exist
- Files.delete(symlink);
- }
- }
-
- private void ensureIsEmpty(Path dir) throws IOException {
- try (var ds = Files.newDirectoryStream(dir)) {
- if (ds.iterator().hasNext()){
- throw new DirectoryNotEmptyException(dir.toString());
- }
- }
- }
-}
diff --git a/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java
deleted file mode 100644
index bcda3d8f2..000000000
--- a/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package org.cryptomator.common.mountpoint;
-
-import org.cryptomator.common.Environment;
-import org.cryptomator.common.settings.VaultSettings;
-import org.cryptomator.common.vaults.Volume;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Optional;
-
-class TemporaryMountPointChooser implements MountPointChooser {
-
- private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
-
- private final VaultSettings vaultSettings;
- private final Environment environment;
- private final MountPointHelper helper;
-
- @Inject
- public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, MountPointHelper helper) {
- this.vaultSettings = vaultSettings;
- this.environment = environment;
- this.helper = helper;
- }
-
- @Override
- public boolean isApplicable(Volume caller) {
- if (this.environment.getMountPointsDir().isEmpty()) {
- LOG.warn("\"cryptomator.mountPointsDir\" is not set to a valid path!");
- return false;
- }
- return true;
- }
-
- @Override
- public Optional chooseMountPoint(Volume caller) {
- assert environment.getMountPointsDir().isPresent();
- //clean leftovers of not-regularly unmounted vaults
- //see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061
- helper.clearIrregularUnmountDebrisIfNeeded();
- return this.environment.getMountPointsDir().map(dir -> this.helper.chooseTemporaryMountPoint(this.vaultSettings, dir));
- }
-
- @Override
- public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
- try {
- switch (caller.getMountPointRequirement()) {
- case PARENT_NO_MOUNT_POINT -> {
- Files.createDirectories(mountPoint.getParent());
- LOG.debug("Successfully created folder for mount point: {}", mountPoint);
- return false;
- }
- case EMPTY_MOUNT_POINT -> {
- Files.createDirectories(mountPoint);
- LOG.debug("Successfully created mount point: {}", mountPoint);
- return true;
- }
- case NONE -> {
- //Requirement "NONE" doesn't make any sense here.
- //No need to prepare/verify a Mountpoint without requiring one...
- throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
- }
- default -> {
- //Currently the case for "UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT"
- throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
- }
- }
- } catch (IOException exception) {
- throw new InvalidMountPointException("IOException while preparing mountpoint", exception);
- }
- }
-
- @Override
- public void cleanup(Volume caller, Path mountPoint) {
- try {
- Files.delete(mountPoint);
- LOG.debug("Successfully deleted mount point: {}", mountPoint);
- } catch (IOException e) {
- LOG.warn("Could not delete mount point: {}", e.getMessage());
- }
- }
-
-}
diff --git a/src/main/java/org/cryptomator/common/settings/Settings.java b/src/main/java/org/cryptomator/common/settings/Settings.java
index 94debddc2..46aed5ea0 100644
--- a/src/main/java/org/cryptomator/common/settings/Settings.java
+++ b/src/main/java/org/cryptomator/common/settings/Settings.java
@@ -36,9 +36,7 @@ public class Settings {
public static final boolean DEFAULT_USE_KEYCHAIN = true;
public static final int DEFAULT_PORT = 42427;
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
- public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV;
public static final boolean DEFAULT_DEBUG_MODE = false;
- public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = VolumeImpl.FUSE;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
@Deprecated // to be changed to "whatever is available" eventually
public static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : "org.cryptomator.linux.keychain.SecretServiceKeychainAccess";
@@ -57,9 +55,7 @@ public class Settings {
private final BooleanProperty useKeychain = new SimpleBooleanProperty(DEFAULT_USE_KEYCHAIN);
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
- private final ObjectProperty preferredGvfsScheme = new SimpleObjectProperty<>(DEFAULT_GVFS_SCHEME);
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
- private final ObjectProperty preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
private final ObjectProperty theme = new SimpleObjectProperty<>(DEFAULT_THEME);
private final ObjectProperty keychainProvider = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_PROVIDER);
private final ObjectProperty userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
@@ -74,6 +70,9 @@ public class Settings {
private final StringProperty language = new SimpleStringProperty(DEFAULT_LANGUAGE);
+ private final StringProperty mountService = new SimpleStringProperty();
+
+
private Consumer saveCmd;
/**
@@ -90,9 +89,7 @@ public class Settings {
useKeychain.addListener(this::somethingChanged);
port.addListener(this::somethingChanged);
numTrayNotifications.addListener(this::somethingChanged);
- preferredGvfsScheme.addListener(this::somethingChanged);
debugMode.addListener(this::somethingChanged);
- preferredVolumeImpl.addListener(this::somethingChanged);
theme.addListener(this::somethingChanged);
keychainProvider.addListener(this::somethingChanged);
userInterfaceOrientation.addListener(this::somethingChanged);
@@ -105,6 +102,7 @@ public class Settings {
windowHeight.addListener(this::somethingChanged);
displayConfiguration.addListener(this::somethingChanged);
language.addListener(this::somethingChanged);
+ mountService.addListener(this::somethingChanged);
}
void setSaveCmd(Consumer saveCmd) {
@@ -153,16 +151,12 @@ public class Settings {
return numTrayNotifications;
}
- public ObjectProperty preferredGvfsScheme() {
- return preferredGvfsScheme;
- }
-
public BooleanProperty debugMode() {
return debugMode;
}
- public ObjectProperty preferredVolumeImpl() {
- return preferredVolumeImpl;
+ public StringProperty mountService() {
+ return mountService;
}
public ObjectProperty theme() {
@@ -210,4 +204,5 @@ public class Settings {
public StringProperty languageProperty() {
return language;
}
+
}
diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
index 03682b60c..b6c5c426c 100644
--- a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
+++ b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
@@ -9,6 +9,7 @@ import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
+import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -44,9 +45,7 @@ public class SettingsJsonAdapter extends TypeAdapter {
out.name("autoCloseVaults").value(value.autoCloseVaults().get());
out.name("port").value(value.port().get());
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
- out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get().name());
out.name("debugMode").value(value.debugMode().get());
- out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
out.name("theme").value(value.theme().get().name());
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
out.name("keychainProvider").value(value.keychainProvider().get());
@@ -60,7 +59,7 @@ public class SettingsJsonAdapter extends TypeAdapter {
out.name("windowHeight").value((value.windowHeightProperty().get()));
out.name("displayConfiguration").value((value.displayConfigurationProperty().get()));
out.name("language").value((value.languageProperty().get()));
-
+ out.name("mountService").value(value.mountService().get());
out.endObject();
}
@@ -75,7 +74,9 @@ public class SettingsJsonAdapter extends TypeAdapter {
@Override
public Settings read(JsonReader in) throws IOException {
Settings settings = new Settings(env);
-
+ //1.6.x legacy
+ String volumeImpl = null;
+ //legacy end
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
@@ -87,9 +88,7 @@ public class SettingsJsonAdapter extends TypeAdapter {
case "autoCloseVaults" -> settings.autoCloseVaults().set(in.nextBoolean());
case "port" -> settings.port().set(in.nextInt());
case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
- case "preferredGvfsScheme" -> settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
case "debugMode" -> settings.debugMode().set(in.nextBoolean());
- case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
case "keychainProvider" -> settings.keychainProvider().set(in.nextString());
@@ -103,33 +102,52 @@ public class SettingsJsonAdapter extends TypeAdapter {
case "windowHeight" -> settings.windowHeightProperty().set(in.nextInt());
case "displayConfiguration" -> settings.displayConfigurationProperty().set(in.nextString());
case "language" -> settings.languageProperty().set(in.nextString());
-
+ case "mountService" -> {
+ var token = in.peek();
+ if (JsonToken.STRING == token) {
+ settings.mountService().set(in.nextString());
+ }
+ }
+ //1.6.x legacy
+ case "preferredVolumeImpl" -> volumeImpl = in.nextString();
+ //legacy end
default -> {
LOG.warn("Unsupported vault setting found in JSON: {}", name);
in.skipValue();
}
}
+
}
in.endObject();
+ //1.6.x legacy
+ if (volumeImpl != null) {
+ settings.mountService().set(convertLegacyVolumeImplToMountService(volumeImpl));
+ }
+ //legacy end
+
return settings;
}
- private VolumeImpl parsePreferredVolumeImplName(String nioAdapterName) {
- try {
- return VolumeImpl.valueOf(nioAdapterName.toUpperCase());
- } catch (IllegalArgumentException e) {
- LOG.warn("Invalid volume type {}. Defaulting to {}.", nioAdapterName, Settings.DEFAULT_PREFERRED_VOLUME_IMPL);
- return Settings.DEFAULT_PREFERRED_VOLUME_IMPL;
- }
- }
-
- private WebDavUrlScheme parseWebDavUrlSchemePrefix(String webDavUrlSchemeName) {
- try {
- return WebDavUrlScheme.valueOf(webDavUrlSchemeName.toUpperCase());
- } catch (IllegalArgumentException e) {
- LOG.warn("Invalid WebDAV url scheme {}. Defaulting to {}.", webDavUrlSchemeName, Settings.DEFAULT_GVFS_SCHEME);
- return Settings.DEFAULT_GVFS_SCHEME;
+ private String convertLegacyVolumeImplToMountService(String volumeImpl) {
+ if (volumeImpl.equals("Dokany")) {
+ return "org.cryptomator.frontend.dokany.mount.DokanyMountProvider";
+ } else if (volumeImpl.equals("FUSE")) {
+ if(SystemUtils.IS_OS_WINDOWS) {
+ return "org.cryptomator.frontend.fuse.mount.WinFspNetworkMountProvider";
+ } else if (SystemUtils.IS_OS_MAC) {
+ return "org.cryptomator.frontend.fuse.mount.MacFuseMountProvider";
+ } else {
+ return "org.cryptomator.frontend.fuse.mount.LinuxFuseMountProvider";
+ }
+ } else {
+ if(SystemUtils.IS_OS_WINDOWS) {
+ return "org.cryptomator.frontend.webdav.mount.WindowsMounter";
+ } else if (SystemUtils.IS_OS_MAC) {
+ return "org.cryptomator.frontend.webdav.mount.MacAppleScriptMounter";
+ } else {
+ return "org.cryptomator.frontend.webdav.mount.LinuxGioMounter";
+ }
}
}
diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/src/main/java/org/cryptomator/common/settings/VaultSettings.java
index 742a4d1b4..3a116b4ce 100644
--- a/src/main/java/org/cryptomator/common/settings/VaultSettings.java
+++ b/src/main/java/org/cryptomator/common/settings/VaultSettings.java
@@ -6,12 +6,10 @@
package org.cryptomator.common.settings;
import com.google.common.base.CharMatcher;
-import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
-import javafx.beans.binding.StringBinding;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
@@ -21,10 +19,8 @@ import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
-import javafx.beans.value.ObservableValue;
import java.nio.file.Path;
import java.util.Objects;
-import java.util.Optional;
import java.util.Random;
/**
@@ -34,7 +30,6 @@ public class VaultSettings {
public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false;
public static final boolean DEFAULT_REVEAL_AFTER_MOUNT = true;
- public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
public static final boolean DEFAULT_USES_READONLY_MODE = false;
public static final String DEFAULT_MOUNT_FLAGS = "";
public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
@@ -47,26 +42,32 @@ public class VaultSettings {
private final String id;
private final ObjectProperty path = new SimpleObjectProperty<>();
private final StringProperty displayName = new SimpleStringProperty();
- private final StringProperty winDriveLetter = new SimpleStringProperty();
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REVEAL_AFTER_MOUNT);
- private final BooleanProperty useCustomMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
- private final StringProperty customMountPath = new SimpleStringProperty();
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
- private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
+ private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS); //TODO: remove empty default mount flags and let this property be null if not used
private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
private final ObjectProperty actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE);
private final IntegerProperty autoLockIdleSeconds = new SimpleIntegerProperty(DEFAULT_AUTOLOCK_IDLE_SECONDS);
private final StringExpression mountName;
+ private final ObjectProperty mountPoint = new SimpleObjectProperty<>();
public VaultSettings(String id) {
this.id = Objects.requireNonNull(id);
- this.mountName = StringExpression.stringExpression(displayName.map(VaultSettings::normalizeDisplayName).orElse(""));
+ this.mountName = StringExpression.stringExpression(Bindings.createStringBinding(() -> {
+ final String name;
+ if (displayName.isEmpty().get()) {
+ name = path.get().getFileName().toString();
+ } else {
+ name = displayName.get();
+ }
+ return normalizeDisplayName(name);
+ }, displayName, path));
}
Observable[] observables() {
- return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock, autoLockWhenIdle, autoLockIdleSeconds};
+ return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode};
}
public static VaultSettings withRandomId() {
@@ -110,18 +111,6 @@ public class VaultSettings {
return mountName;
}
- public StringProperty winDriveLetter() {
- return winDriveLetter;
- }
-
- public Optional getWinDriveLetter() {
- String current = this.winDriveLetter.get();
- if (!Strings.isNullOrEmpty(current)) {
- return Optional.of(current);
- }
- return Optional.empty();
- }
-
public BooleanProperty unlockAfterStartup() {
return unlockAfterStartup;
}
@@ -130,20 +119,12 @@ public class VaultSettings {
return revealAfterMount;
}
- public BooleanProperty useCustomMountPath() {
- return useCustomMountPath;
+ public Path getMountPoint() {
+ return mountPoint.get();
}
- public StringProperty customMountPath() {
- return customMountPath;
- }
-
- public Optional getCustomMountPath() {
- if (useCustomMountPath.get()) {
- return Optional.ofNullable(Strings.emptyToNull(customMountPath.get()));
- } else {
- return Optional.empty();
- }
+ public ObjectProperty mountPoint() {
+ return mountPoint;
}
public BooleanProperty usesReadOnlyMode() {
@@ -189,5 +170,4 @@ public class VaultSettings {
return false;
}
}
-
}
diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java
index a3a3118dc..cffeb3aa7 100644
--- a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java
+++ b/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java
@@ -6,11 +6,14 @@
package org.cryptomator.common.settings;
import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
import java.nio.file.Paths;
class VaultSettingsJsonAdapter {
@@ -22,11 +25,10 @@ class VaultSettingsJsonAdapter {
out.name("id").value(value.getId());
out.name("path").value(value.path().get().toString());
out.name("displayName").value(value.displayName().get());
- out.name("winDriveLetter").value(value.winDriveLetter().get());
out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
out.name("revealAfterMount").value(value.revealAfterMount().get());
- out.name("useCustomMountPath").value(value.useCustomMountPath().get());
- out.name("customMountPath").value(value.customMountPath().get());
+ var mountPoint = value.mountPoint().get();
+ out.name("mountPoint").value(mountPoint != null ? mountPoint.toAbsolutePath().toString() : null);
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
out.name("mountFlags").value(value.mountFlags().get());
out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get());
@@ -41,18 +43,22 @@ class VaultSettingsJsonAdapter {
String path = null;
String mountName = null; //see https://github.com/cryptomator/cryptomator/pull/1318
String displayName = null;
- String customMountPath = null;
- String winDriveLetter = null;
boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
boolean revealAfterMount = VaultSettings.DEFAULT_REVEAL_AFTER_MOUNT;
- boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
+ Path mountPoint = null;
int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
boolean autoLockWhenIdle = VaultSettings.DEFAULT_AUTOLOCK_WHEN_IDLE;
int autoLockIdleSeconds = VaultSettings.DEFAULT_AUTOLOCK_IDLE_SECONDS;
+ //legacy from 1.6.x
+ boolean useCustomMountPath = false;
+ String customMountPath = "";
+ String winDriveLetter = "";
+ //legacy end
+
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
@@ -61,17 +67,26 @@ class VaultSettingsJsonAdapter {
case "path" -> path = in.nextString();
case "mountName" -> mountName = in.nextString(); //see https://github.com/cryptomator/cryptomator/pull/1318
case "displayName" -> displayName = in.nextString();
- case "winDriveLetter" -> winDriveLetter = in.nextString();
case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean();
case "revealAfterMount" -> revealAfterMount = in.nextBoolean();
- case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
- case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
case "mountFlags" -> mountFlags = in.nextString();
+ case "mountPoint" -> {
+ if (JsonToken.NULL == in.peek()) {
+ in.nextNull();
+ } else {
+ mountPoint = parseMountPoint(in.nextString());
+ }
+ }
case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
case "autoLockWhenIdle" -> autoLockWhenIdle = in.nextBoolean();
case "autoLockIdleSeconds" -> autoLockIdleSeconds = in.nextInt();
+ //legacy from 1.6.x
+ case "winDriveLetter" -> winDriveLetter = in.nextString();
+ case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
+ case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
+ //legacy end
default -> {
LOG.warn("Unsupported vault setting found in JSON: {}", name);
in.skipValue();
@@ -87,20 +102,34 @@ class VaultSettingsJsonAdapter {
vaultSettings.displayName().set(mountName);
}
vaultSettings.path().set(Paths.get(path));
- vaultSettings.winDriveLetter().set(winDriveLetter);
vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
vaultSettings.revealAfterMount().set(revealAfterMount);
- vaultSettings.useCustomMountPath().set(useCustomMountPath);
- vaultSettings.customMountPath().set(customMountPath);
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
vaultSettings.mountFlags().set(mountFlags);
vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
vaultSettings.autoLockWhenIdle().set(autoLockWhenIdle);
vaultSettings.autoLockIdleSeconds().set(autoLockIdleSeconds);
+ vaultSettings.mountPoint().set(mountPoint);
+ //legacy from 1.6.x
+ if(useCustomMountPath && !customMountPath.isBlank()) {
+ vaultSettings.mountPoint().set(parseMountPoint(customMountPath));
+ } else if(!winDriveLetter.isBlank() ) {
+ vaultSettings.mountPoint().set(parseMountPoint(winDriveLetter+":\\"));
+ }
+ //legacy end
return vaultSettings;
}
+ private Path parseMountPoint(String mountPoint) {
+ try {
+ return Path.of(mountPoint);
+ } catch (InvalidPathException e) {
+ LOG.warn("Invalid string as mount point. Defaulting to null.");
+ return null;
+ }
+ }
+
private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) {
try {
return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase());
diff --git a/src/main/java/org/cryptomator/common/settings/VolumeImpl.java b/src/main/java/org/cryptomator/common/settings/VolumeImpl.java
deleted file mode 100644
index 473596fc4..000000000
--- a/src/main/java/org/cryptomator/common/settings/VolumeImpl.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.cryptomator.common.settings;
-
-public enum VolumeImpl {
- WEBDAV("WebDAV"),
- FUSE("FUSE"),
- DOKANY("Dokany");
-
- private String displayName;
-
- VolumeImpl(String displayName) {
- this.displayName = displayName;
- }
-
- public String getDisplayName() {
- return displayName;
- }
-
-}
diff --git a/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java b/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java
deleted file mode 100644
index a72d11968..000000000
--- a/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.cryptomator.common.settings;
-
-public enum WebDavUrlScheme {
- DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
- WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");
-
- private final String prefix;
- private final String displayName;
-
- WebDavUrlScheme(String prefix, String displayName) {
- this.prefix = prefix;
- this.displayName = displayName;
- }
-
- public String getPrefix() {
- return prefix;
- }
-
- public String getDisplayName() {
- return displayName;
- }
-}
diff --git a/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java b/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java
deleted file mode 100644
index d66ad19d8..000000000
--- a/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.cryptomator.common.vaults;
-
-import com.google.common.collect.Iterables;
-import org.cryptomator.common.mountpoint.InvalidMountPointException;
-import org.cryptomator.common.mountpoint.MountPointChooser;
-
-import java.nio.file.Path;
-import java.util.Optional;
-
-public abstract class AbstractVolume implements Volume {
-
- private final Iterable choosers;
-
- protected Path mountPoint;
- private boolean cleanupRequired;
- private MountPointChooser usedChooser;
-
- public AbstractVolume(Iterable choosers) {
- this.choosers = choosers;
- }
-
- protected Path determineMountPoint() throws InvalidMountPointException {
- var applicableChoosers = Iterables.filter(choosers, c -> c.isApplicable(this));
- for (var chooser : applicableChoosers) {
- Optional chosenPath = chooser.chooseMountPoint(this);
- if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint
- continue;
- }
- this.cleanupRequired = chooser.prepare(this, chosenPath.get());
- this.usedChooser = chooser;
- return chosenPath.get();
- }
- throw new InvalidMountPointException(String.format("No feasible MountPoint found by choosers: %s", applicableChoosers));
- }
-
- protected void cleanupMountPoint() {
- if (this.cleanupRequired) {
- this.usedChooser.cleanup(this, this.mountPoint);
- }
- }
-
- @Override
- public Optional getMountPoint() {
- return Optional.ofNullable(mountPoint);
- }
-}
diff --git a/src/main/java/org/cryptomator/common/vaults/AutoLocker.java b/src/main/java/org/cryptomator/common/vaults/AutoLocker.java
index d113f4177..e9ef22bae 100644
--- a/src/main/java/org/cryptomator/common/vaults/AutoLocker.java
+++ b/src/main/java/org/cryptomator/common/vaults/AutoLocker.java
@@ -1,11 +1,13 @@
package org.cryptomator.common.vaults;
+import org.cryptomator.integrations.mount.UnmountFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javafx.collections.ObservableList;
+import java.io.IOException;
import java.time.Instant;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -39,7 +41,7 @@ public class AutoLocker {
try {
vault.lock(false);
LOG.info("Autolocked {} after idle timeout", vault.getDisplayName());
- } catch (Volume.VolumeException | LockNotCompletedException e) {
+ } catch (UnmountFailedException | IOException e) {
LOG.error("Autolocking failed.", e);
}
}
diff --git a/src/main/java/org/cryptomator/common/vaults/DefaultMountFlags.java b/src/main/java/org/cryptomator/common/vaults/DefaultMountFlags.java
deleted file mode 100644
index 4f3a8ff15..000000000
--- a/src/main/java/org/cryptomator/common/vaults/DefaultMountFlags.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.cryptomator.common.vaults;
-
-import javax.inject.Qualifier;
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-@Qualifier
-@Documented
-@Retention(RUNTIME)
-@interface DefaultMountFlags {
-
-}
diff --git a/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java b/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java
deleted file mode 100644
index c08642073..000000000
--- a/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package org.cryptomator.common.vaults;
-
-import org.cryptomator.common.mountpoint.InvalidMountPointException;
-import org.cryptomator.common.mountpoint.MountPointChooser;
-import org.cryptomator.common.settings.VaultSettings;
-import org.cryptomator.common.settings.VolumeImpl;
-import org.cryptomator.cryptofs.CryptoFileSystem;
-import org.cryptomator.frontend.dokany.DokanyMountFailedException;
-import org.cryptomator.frontend.dokany.Mount;
-import org.cryptomator.frontend.dokany.MountFactory;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import java.util.function.Consumer;
-
-public class DokanyVolume extends AbstractVolume {
-
- private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class);
-
- private static final String FS_TYPE_NAME = "CryptomatorFS";
-
- private final VaultSettings vaultSettings;
-
- private Mount mount;
-
- @Inject
- public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable choosers) {
- super(choosers);
- this.vaultSettings = vaultSettings;
- }
-
- @Override
- public VolumeImpl getImplementationType() {
- return VolumeImpl.DOKANY;
- }
-
- @Override
- public void mount(CryptoFileSystem fs, String mountFlags, Consumer onExitAction) throws InvalidMountPointException, VolumeException {
- this.mountPoint = determineMountPoint();
- try {
- this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction);
- } catch (DokanyMountFailedException e) {
- if (vaultSettings.getCustomMountPath().isPresent()) {
- LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
- }
- throw new VolumeException("Unable to mount Filesystem", e);
- }
- }
-
- @Override
- public void reveal(Revealer revealer) throws VolumeException {
- try {
- mount.reveal(revealer::reveal);
- } catch (Exception e) {
- throw new VolumeException(e);
- }
- }
-
- @Override
- public void unmount() throws VolumeException {
- try {
- mount.unmount();
- } catch (IllegalStateException e) {
- throw new VolumeException("Unmount Failed.", e);
- }
- cleanupMountPoint();
- }
-
- @Override
- public void unmountForced() {
- mount.unmountForced();
- cleanupMountPoint();
- }
-
- @Override
- public boolean supportsForcedUnmount() {
- return true;
- }
-
- @Override
- public boolean isSupported() {
- return DokanyVolume.isSupportedStatic();
- }
-
- @Override
- public MountPointRequirement getMountPointRequirement() {
- return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.EMPTY_MOUNT_POINT;
- }
-
- public static boolean isSupportedStatic() {
- return MountFactory.isApplicable();
- }
-}
diff --git a/src/main/java/org/cryptomator/common/vaults/FuseVolume.java b/src/main/java/org/cryptomator/common/vaults/FuseVolume.java
deleted file mode 100644
index 3bc869592..000000000
--- a/src/main/java/org/cryptomator/common/vaults/FuseVolume.java
+++ /dev/null
@@ -1,133 +0,0 @@
-package org.cryptomator.common.vaults;
-
-import com.google.common.collect.Iterators;
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.mountpoint.InvalidMountPointException;
-import org.cryptomator.common.mountpoint.MountPointChooser;
-import org.cryptomator.common.settings.VaultSettings;
-import org.cryptomator.common.settings.VolumeImpl;
-import org.cryptomator.cryptofs.CryptoFileSystem;
-import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
-import org.cryptomator.frontend.fuse.mount.FuseMountException;
-import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
-import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
-import org.cryptomator.frontend.fuse.mount.Mount;
-import org.cryptomator.frontend.fuse.mount.Mounter;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.regex.Pattern;
-
-public class FuseVolume extends AbstractVolume {
-
- private static final Pattern NON_WHITESPACE_OR_QUOTED = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); // Thanks to https://stackoverflow.com/a/366532
-
- private final VaultSettings vaultSettings;
-
- private Mount mount;
-
- @Inject
- public FuseVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable choosers) {
- super(choosers);
- this.vaultSettings = vaultSettings;
- }
-
- @Override
- public void mount(CryptoFileSystem fs, String mountFlags, Consumer onExitAction) throws InvalidMountPointException, VolumeException {
- this.mountPoint = determineMountPoint();
- mount(fs.getPath("/"), mountFlags, onExitAction);
- }
-
- private void mount(Path root, String mountFlags, Consumer onExitAction) throws VolumeException {
- try {
- Mounter mounter = FuseMountFactory.getMounter();
- EnvironmentVariables envVars = EnvironmentVariables.create() //
- .withFlags(splitFlags(mountFlags)) //
- .withMountPoint(mountPoint) //
- .withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
- .build();
- this.mount = mounter.mount(root, envVars, onExitAction);
- } catch (FuseMountException | FuseNotSupportedException e) {
- throw new VolumeException("Unable to mount Filesystem", e);
- }
- }
-
- private String[] splitFlags(String str) {
- List flags = new ArrayList<>();
- var matches = Iterators.peekingIterator(NON_WHITESPACE_OR_QUOTED.matcher(str).results().iterator());
- while (matches.hasNext()) {
- String flag = matches.next().group();
- // check if flag is missing its argument:
- if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(1) != null) { // next is "double quoted"
- // next is "double quoted" and flag is missing its argument
- flag += matches.next().group(1);
- } else if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(2) != null) {
- // next is 'single quoted' and flag is missing its argument
- flag += matches.next().group(2);
- }
- flags.add(flag);
- }
- return flags.toArray(String[]::new);
- }
-
- @Override
- public void reveal(Revealer revealer) throws VolumeException {
- try {
- mount.reveal(revealer::reveal);
- } catch (Exception e) {
- throw new VolumeException(e);
- }
- }
-
- @Override
- public boolean supportsForcedUnmount() {
- return true;
- }
-
- @Override
- public synchronized void unmountForced() throws VolumeException {
- try {
- mount.unmountForced();
- } catch (FuseMountException e) {
- throw new VolumeException(e);
- }
- cleanupMountPoint();
- }
-
- @Override
- public synchronized void unmount() throws VolumeException {
- try {
- mount.unmount();
- } catch (FuseMountException e) {
- throw new VolumeException(e);
- }
- cleanupMountPoint();
- }
-
- @Override
- public boolean isSupported() {
- return FuseVolume.isSupportedStatic();
- }
-
- @Override
- public VolumeImpl getImplementationType() {
- return VolumeImpl.FUSE;
- }
-
- @Override
- public MountPointRequirement getMountPointRequirement() {
- if (!SystemUtils.IS_OS_WINDOWS) {
- return MountPointRequirement.EMPTY_MOUNT_POINT;
- }
- return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.PARENT_NO_MOUNT_POINT;
- }
-
- public static boolean isSupportedStatic() {
- return FuseMountFactory.isFuseSupported();
- }
-
-}
diff --git a/src/main/java/org/cryptomator/common/vaults/LockNotCompletedException.java b/src/main/java/org/cryptomator/common/vaults/LockNotCompletedException.java
deleted file mode 100644
index 237630da0..000000000
--- a/src/main/java/org/cryptomator/common/vaults/LockNotCompletedException.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.cryptomator.common.vaults;
-
-public class LockNotCompletedException extends Exception {
-
- public LockNotCompletedException(String reason) {
- super(reason);
- }
-
- public LockNotCompletedException(Throwable cause) {
- super(cause);
- }
-}
diff --git a/src/main/java/org/cryptomator/common/vaults/MountPointRequirement.java b/src/main/java/org/cryptomator/common/vaults/MountPointRequirement.java
deleted file mode 100644
index deec61e1a..000000000
--- a/src/main/java/org/cryptomator/common/vaults/MountPointRequirement.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.cryptomator.common.vaults;
-
-/**
- * Enumeration used to indicate the requirements for mounting a vault
- * using a specific {@link Volume VolumeProvider}, e.g. {@link FuseVolume}.
- */
-public enum MountPointRequirement {
-
- /**
- * The Mountpoint needs to be a filesystem root and must not exist.
- */
- UNUSED_ROOT_DIR,
-
- /**
- * No Mountpoint on the local filesystem required. (e.g. WebDAV)
- */
- NONE,
-
- /**
- * A parent folder is required, but the actual Mountpoint must not exist.
- */
- PARENT_NO_MOUNT_POINT,
-
- /**
- * A parent folder is required, but the actual Mountpoint may exist.
- */
- PARENT_OPT_MOUNT_POINT,
-
- /**
- * The actual Mountpoint must exist and must be empty.
- */
- EMPTY_MOUNT_POINT;
-}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java
index 898bf1a51..e4dbd9b2c 100644
--- a/src/main/java/org/cryptomator/common/vaults/Vault.java
+++ b/src/main/java/org/cryptomator/common/vaults/Vault.java
@@ -8,12 +8,11 @@
*******************************************************************************/
package org.cryptomator.common.vaults;
-import com.google.common.base.Strings;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Constants;
-import org.cryptomator.common.mountpoint.InvalidMountPointException;
+import org.cryptomator.common.mount.Mounter;
+import org.cryptomator.common.mount.WindowsDriveLetters;
import org.cryptomator.common.settings.VaultSettings;
-import org.cryptomator.common.vaults.Volume.VolumeException;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
@@ -22,15 +21,18 @@ import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.cryptomator.integrations.mount.MountFailedException;
+import org.cryptomator.integrations.mount.Mountpoint;
+import org.cryptomator.integrations.mount.UnmountFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
-import javax.inject.Provider;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
+import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
@@ -41,9 +43,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
import java.util.Objects;
-import java.util.Optional;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@PerVault
@@ -54,8 +54,6 @@ public class Vault {
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
private final VaultSettings vaultSettings;
- private final Provider volumeProvider;
- private final StringBinding defaultMountFlags;
private final AtomicReference cryptoFileSystem;
private final VaultState state;
private final ObjectProperty lastKnownException;
@@ -68,18 +66,16 @@ public class Vault {
private final BooleanBinding missing;
private final BooleanBinding needsMigration;
private final BooleanBinding unknownError;
- private final StringBinding accessPoint;
- private final BooleanBinding accessPointPresent;
+ private final ObjectBinding mountPoint;
+ private final Mounter mounter;
private final BooleanProperty showingStats;
- private volatile Volume volume;
+ private AtomicReference mountHandle = new AtomicReference<>(null);
@Inject
- Vault(VaultSettings vaultSettings, VaultConfigCache configCache, Provider volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty lastKnownException, VaultStats stats) {
+ Vault(VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty lastKnownException, VaultStats stats, WindowsDriveLetters windowsDriveLetters, Mounter mounter) {
this.vaultSettings = vaultSettings;
this.configCache = configCache;
- this.volumeProvider = volumeProvider;
- this.defaultMountFlags = defaultMountFlags;
this.cryptoFileSystem = cryptoFileSystem;
this.state = state;
this.lastKnownException = lastKnownException;
@@ -91,8 +87,8 @@ public class Vault {
this.missing = Bindings.createBooleanBinding(this::isMissing, state);
this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
- this.accessPoint = Bindings.createStringBinding(this::getAccessPoint, state);
- this.accessPointPresent = this.accessPoint.isNotEmpty();
+ this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state);
+ this.mounter = mounter;
this.showingStats = new SimpleBooleanProperty(false);
}
@@ -142,7 +138,7 @@ public class Vault {
}
}
- public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
+ public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, MountFailedException {
if (cryptoFileSystem.get() != null) {
throw new IllegalStateException("Already unlocked.");
}
@@ -150,9 +146,9 @@ public class Vault {
boolean success = false;
try {
cryptoFileSystem.set(fs);
- volume = volumeProvider.get();
- volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit);
- success = true;
+ var rootPath = fs.getRootDirectories().iterator().next();
+ var mountHandle = mounter.mount(vaultSettings, rootPath);
+ success = this.mountHandle.compareAndSet(null, mountHandle);
} finally {
if (!success) {
destroyCryptoFileSystem();
@@ -160,37 +156,28 @@ public class Vault {
}
}
- private void lockOnVolumeExit(Throwable t) {
- LOG.info("Unmounted vault '{}'", getDisplayName());
- destroyCryptoFileSystem();
- state.set(VaultState.Value.LOCKED);
- if (t != null) {
- LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
+ public synchronized void lock(boolean forced) throws UnmountFailedException, IOException {
+ var mountHandle = this.mountHandle.get();
+ if (mountHandle == null) {
+ //TODO: noop or InvalidStateException?
+ return;
}
- }
- public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException {
- //initiate unmount
- if (forced && volume.supportsForcedUnmount()) {
- volume.unmountForced();
+ if (forced && mountHandle.supportsUnmountForced()) {
+ mountHandle.mountObj().unmountForced();
} else {
- volume.unmount();
+ mountHandle.mountObj().unmount();
}
- //wait for lockOnVolumeExit to be executed
try {
- boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS);
- if (!locked) {
- throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress.");
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new LockNotCompletedException(e);
+ mountHandle.mountObj().close();
+ mountHandle.specialCleanup().run();
+ } finally {
+ destroyCryptoFileSystem();
}
- }
- public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
- volume.reveal(vaultRevealer);
+ this.mountHandle.set(null);
+ LOG.info("Locked vault '{}'", getDisplayName());
}
// ******************************************************************************
@@ -273,25 +260,13 @@ public class Vault {
return vaultSettings.displayName().get();
}
- public StringBinding accessPointProperty() {
- return accessPoint;
+ public ObjectBinding mountPointProperty() {
+ return mountPoint;
}
- public String getAccessPoint() {
- if (state.getValue() == VaultState.Value.UNLOCKED) {
- assert volume != null;
- return volume.getMountPoint().orElse(Path.of("")).toString();
- } else {
- return "";
- }
- }
-
- public BooleanBinding accessPointPresentProperty() {
- return accessPointPresent;
- }
-
- public boolean isAccessPointPresent() {
- return accessPointPresent.get();
+ public Mountpoint getMountPoint() {
+ var handle = mountHandle.get();
+ return handle == null ? null : handle.mountObj().getMountpoint();
}
public StringBinding displayablePathProperty() {
@@ -314,7 +289,7 @@ public class Vault {
}
public boolean isShowingStats() {
- return accessPointPresent.get();
+ return mountHandle.get() != null;
}
@@ -339,7 +314,6 @@ public class Vault {
return vaultSettings.path().getValue();
}
-
/**
* Gets from the cleartext path its ciphertext counterpart.
* The cleartext path has to start from the vault root (by starting with "/").
@@ -356,43 +330,14 @@ public class Vault {
return fs.getCiphertextPath(cryptoPath);
}
- public boolean isHavingCustomMountFlags() {
- return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
- }
-
- public StringBinding defaultMountFlagsProperty() {
- return defaultMountFlags;
- }
-
- public String getDefaultMountFlags() {
- return defaultMountFlags.get();
- }
-
- public String getEffectiveMountFlags() {
- String mountFlags = vaultSettings.mountFlags().get();
- if (Strings.isNullOrEmpty(mountFlags)) {
- return getDefaultMountFlags();
- } else {
- return mountFlags;
- }
- }
-
public VaultConfigCache getVaultConfigCache() {
return configCache;
}
- public void setCustomMountFlags(String mountFlags) {
- vaultSettings.mountFlags().set(mountFlags);
- }
-
public String getId() {
return vaultSettings.getId();
}
- public Optional getVolume() {
- return Optional.ofNullable(this.volume);
- }
-
// ******************************************************************************
// Hashcode / Equals
// *******************************************************************************/
@@ -412,6 +357,11 @@ public class Vault {
}
public boolean supportsForcedUnmount() {
- return volume.supportsForcedUnmount();
+ var mh = mountHandle.get();
+ if (mh == null) {
+ throw new IllegalStateException("Vault is not mounted");
+ }
+ return mountHandle.get().supportsUnmountForced();
}
+
}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/common/vaults/VaultComponent.java b/src/main/java/org/cryptomator/common/vaults/VaultComponent.java
index ae525d7b0..ac44e8aad 100644
--- a/src/main/java/org/cryptomator/common/vaults/VaultComponent.java
+++ b/src/main/java/org/cryptomator/common/vaults/VaultComponent.java
@@ -8,13 +8,12 @@ package org.cryptomator.common.vaults;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.Nullable;
-import org.cryptomator.common.mountpoint.MountPointChooserModule;
import org.cryptomator.common.settings.VaultSettings;
import javax.inject.Named;
@PerVault
-@Subcomponent(modules = {VaultModule.class, MountPointChooserModule.class})
+@Subcomponent(modules = {VaultModule.class})
public interface VaultComponent {
Vault vault();
diff --git a/src/main/java/org/cryptomator/common/vaults/VaultModule.java b/src/main/java/org/cryptomator/common/vaults/VaultModule.java
index 902b2d872..c8853fa5b 100644
--- a/src/main/java/org/cryptomator/common/vaults/VaultModule.java
+++ b/src/main/java/org/cryptomator/common/vaults/VaultModule.java
@@ -7,27 +7,14 @@ package org.cryptomator.common.vaults;
import dagger.Module;
import dagger.Provides;
-import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Nullable;
-import org.cryptomator.common.settings.Settings;
-import org.cryptomator.common.settings.VaultSettings;
-import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.StringBinding;
-import javafx.beans.binding.StringExpression;
-import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicReference;
@Module
@@ -48,130 +35,4 @@ public class VaultModule {
return new SimpleObjectProperty<>(initialErrorCause);
}
- @Provides
- public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
- VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
- if (VolumeImpl.DOKANY == preferredImpl && dokanyVolume.isSupported()) {
- return dokanyVolume;
- } else if (VolumeImpl.FUSE == preferredImpl && fuseVolume.isSupported()) {
- return fuseVolume;
- } else {
- if (VolumeImpl.WEBDAV != preferredImpl) {
- LOG.warn("Using WebDAV, because {} is not supported.", preferredImpl.getDisplayName());
- }
- assert webDavVolume.isSupported();
- return webDavVolume;
- }
- }
-
- @Provides
- @PerVault
- @DefaultMountFlags
- public StringBinding provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
- ObjectProperty preferredVolumeImpl = settings.preferredVolumeImpl();
- StringExpression mountName = vaultSettings.mountName();
- BooleanProperty readOnly = vaultSettings.usesReadOnlyMode();
-
- return Bindings.createStringBinding(() -> {
- VolumeImpl v = preferredVolumeImpl.get();
- if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_MAC) {
- return getMacFuseDefaultMountFlags(mountName, readOnly);
- } else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_LINUX) {
- return getLinuxFuseDefaultMountFlags(readOnly);
- } else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_WINDOWS) {
- return getWindowsFuseDefaultMountFlags(mountName, readOnly);
- } else if (v == VolumeImpl.DOKANY && SystemUtils.IS_OS_WINDOWS) {
- return getDokanyDefaultMountFlags(readOnly);
- } else {
- return "--flags-supported-on-FUSE-or-DOKANY-only";
- }
- }, mountName, readOnly, preferredVolumeImpl);
- }
-
- // see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
- private String getMacFuseDefaultMountFlags(StringExpression mountName, ReadOnlyBooleanProperty readOnly) {
- assert SystemUtils.IS_OS_MAC_OSX;
- StringBuilder flags = new StringBuilder();
- if (readOnly.get()) {
- flags.append(" -ordonly");
- }
- flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
- flags.append(" -oatomic_o_trunc");
- flags.append(" -oauto_xattr");
- flags.append(" -oauto_cache");
- flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
- flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
-
- try {
- Path userHome = Paths.get(System.getProperty("user.home"));
- int uid = (int) Files.getAttribute(userHome, "unix:uid");
- int gid = (int) Files.getAttribute(userHome, "unix:gid");
- flags.append(" -ouid=").append(uid);
- flags.append(" -ogid=").append(gid);
- } catch (IOException e) {
- LOG.error("Could not read uid/gid from USER_HOME", e);
- }
-
- return flags.toString().strip();
- }
-
- // see https://manpages.debian.org/testing/fuse/mount.fuse.8.en.html
- private String getLinuxFuseDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
- assert SystemUtils.IS_OS_LINUX;
- StringBuilder flags = new StringBuilder();
- if (readOnly.get()) {
- flags.append(" -oro");
- }
- flags.append(" -oauto_unmount");
-
- try {
- Path userHome = Paths.get(System.getProperty("user.home"));
- int uid = (int) Files.getAttribute(userHome, "unix:uid");
- int gid = (int) Files.getAttribute(userHome, "unix:gid");
- flags.append(" -ouid=").append(uid);
- flags.append(" -ogid=").append(gid);
- } catch (IOException e) {
- LOG.error("Could not read uid/gid from USER_HOME", e);
- }
-
- return flags.toString().strip();
- }
-
- // see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse_main.c#L53-L62 for syntax guide
- // see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse.c#L295-L319 for options (-o <...>)
- // see https://github.com/billziss-gh/winfsp/wiki/Frequently-Asked-Questions/5ba00e4be4f5e938eaae6ef1500b331de12dee77 (FUSE 4.) on why the given defaults were chosen
- private String getWindowsFuseDefaultMountFlags(StringExpression mountName, ReadOnlyBooleanProperty readOnly) {
- assert SystemUtils.IS_OS_WINDOWS;
- StringBuilder flags = new StringBuilder();
-
- //WinFSP has no explicit "readonly"-option, nut not setting the group/user-id has the same effect, tho.
- //So for the time being not setting them is the way to go...
- //See: https://github.com/billziss-gh/winfsp/issues/319
- if (!readOnly.get()) {
- flags.append(" -ouid=-1");
- flags.append(" -ogid=11");
- }
- flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
- //Dokany requires this option to be set, WinFSP doesn't seem to share this peculiarity,
- //but the option exists. Let's keep this here in case we need it.
-// flags.append(" -oThreadCount=").append(5);
-
- return flags.toString().strip();
- }
-
- // see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
- private String getDokanyDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
- assert SystemUtils.IS_OS_WINDOWS;
- StringBuilder flags = new StringBuilder();
- flags.append(" --options CURRENT_SESSION");
- if (readOnly.get()) {
- flags.append(",WRITE_PROTECTION");
- }
- flags.append(" --thread-count 5");
- flags.append(" --timeout 10000");
- flags.append(" --allocation-unit-size 4096");
- flags.append(" --sector-size 4096");
- return flags.toString().strip();
- }
-
}
diff --git a/src/main/java/org/cryptomator/common/vaults/Volume.java b/src/main/java/org/cryptomator/common/vaults/Volume.java
deleted file mode 100644
index 5f434fa43..000000000
--- a/src/main/java/org/cryptomator/common/vaults/Volume.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package org.cryptomator.common.vaults;
-
-import org.cryptomator.common.mountpoint.InvalidMountPointException;
-import org.cryptomator.common.settings.VolumeImpl;
-import org.cryptomator.cryptofs.CryptoFileSystem;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Optional;
-import java.util.concurrent.CompletionStage;
-import java.util.function.Consumer;
-import java.util.stream.Stream;
-
-/**
- * Takes a Volume and uses it to mount an unlocked vault
- */
-public interface Volume {
-
- /**
- * Checks in constant time whether this volume type is supported on the system running Cryptomator.
- *
- * @return true if this volume can be mounted
- */
- boolean isSupported();
-
- /**
- * Gets the corresponding enum type of the {@link VolumeImpl volume implementation ("VolumeImpl")} that is implemented by this Volume.
- *
- * @return the type of implementation as defined by the {@link VolumeImpl VolumeImpl enum}
- */
- VolumeImpl getImplementationType();
-
- /**
- * @param fs
- * @throws IOException
- */
- void mount(CryptoFileSystem fs, String mountFlags, Consumer onExitAction) throws IOException, VolumeException, InvalidMountPointException;
-
- /**
- * Reveals the mounted volume.
- *
- * The given {@code revealer} might be used to do it, but not necessarily.
- *
- * @param revealer An object capable of revealing the location of the mounted vault to view the content (e.g. in the default file browser).
- * @throws VolumeException
- */
- void reveal(Revealer revealer) throws VolumeException;
-
- void unmount() throws VolumeException;
-
- Optional getMountPoint();
-
- MountPointRequirement getMountPointRequirement();
-
- // optional forced unmounting:
-
- default boolean supportsForcedUnmount() {
- return false;
- }
-
- default void unmountForced() throws VolumeException {
- throw new VolumeException("Operation not supported.");
- }
-
- static VolumeImpl[] getCurrentSupportedAdapters() {
- return Stream.of(VolumeImpl.values()).filter(impl -> switch (impl) {
- case WEBDAV -> WebDavVolume.isSupportedStatic();
- case DOKANY -> DokanyVolume.isSupportedStatic();
- case FUSE -> FuseVolume.isSupportedStatic();
- }).toArray(VolumeImpl[]::new);
- }
-
- /**
- * Exception thrown when a volume-specific command such as mount/unmount/reveal failed.
- */
- class VolumeException extends Exception {
-
- public VolumeException(String message) {
- super(message);
- }
-
- public VolumeException(Throwable cause) {
- super(cause);
- }
-
- public VolumeException(String message, Throwable cause) {
- super(message, cause);
- }
-
- }
-
- /**
- * Hides and unifies the different Revealer implementations in the different nio-adapters.
- */
- @FunctionalInterface
- interface Revealer {
-
- void reveal(Path p) throws VolumeException;
-
- }
-
-}
diff --git a/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java b/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
deleted file mode 100644
index af7ca10f2..000000000
--- a/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
+++ /dev/null
@@ -1,172 +0,0 @@
-package org.cryptomator.common.vaults;
-
-
-import com.google.common.base.CharMatcher;
-import org.cryptomator.common.Environment;
-import org.cryptomator.common.settings.Settings;
-import org.cryptomator.common.settings.VaultSettings;
-import org.cryptomator.common.settings.VolumeImpl;
-import org.cryptomator.cryptofs.CryptoFileSystem;
-import org.cryptomator.frontend.webdav.WebDavServer;
-import org.cryptomator.frontend.webdav.mount.MountParams;
-import org.cryptomator.frontend.webdav.mount.Mounter;
-import org.cryptomator.frontend.webdav.servlet.WebDavServletController;
-
-import javax.inject.Inject;
-import javax.inject.Provider;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.nio.file.Path;
-import java.util.Optional;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-public class WebDavVolume implements Volume {
-
- private final Provider serverProvider;
- private final VaultSettings vaultSettings;
- private final Settings settings;
- private final WindowsDriveLetters windowsDriveLetters;
- private final Environment environment;
-
- private WebDavServer server;
- private WebDavServletController servlet;
- private Mounter.Mount mount;
- private Consumer onExitAction;
-
- @Inject
- public WebDavVolume(Provider serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters, Environment environment) {
- this.serverProvider = serverProvider;
- this.vaultSettings = vaultSettings;
- this.settings = settings;
- this.windowsDriveLetters = windowsDriveLetters;
- this.environment = environment;
- }
-
- @Override
- public void mount(CryptoFileSystem fs, String mountFlags, Consumer onExitAction) throws VolumeException {
- startServlet(fs);
- mountServlet();
- this.onExitAction = onExitAction;
- }
-
- private void startServlet(CryptoFileSystem fs) {
- if (server == null) {
- server = serverProvider.get();
- }
- if (!server.isRunning()) {
- server.start();
- }
- CharMatcher acceptable = CharMatcher.inRange('0', '9').or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.inRange('a', 'z'));
- String urlConformMountName = acceptable.negate().collapseFrom(vaultSettings.mountName().get(), '_');
- servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + urlConformMountName);
- servlet.start();
- }
-
- private void mountServlet() throws VolumeException {
- if (servlet == null) {
- throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
- }
-
- //on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specific one or there is no free.
- Supplier driveLetterSupplier;
- if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
- driveLetterSupplier = () -> windowsDriveLetters.getDesiredAvailableDriveLetter().orElse(null);
- } else {
- driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
- }
-
- MountParams mountParams = MountParams.create() //
- .withWindowsDriveLetter(driveLetterSupplier.get()) //
- .withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
- .withWebdavHostname(getLocalhostAliasOrNull()) //
- .build();
- try {
- this.mount = servlet.mount(mountParams); // might block this thread for a while
- } catch (Mounter.CommandFailedException e) {
- throw new VolumeException(e);
- }
- }
-
- @Override
- public void reveal(Revealer revealer) throws VolumeException {
- try {
- mount.reveal(revealer::reveal);
- } catch (Exception e) {
- throw new VolumeException(e);
- }
- }
-
- @Override
- public synchronized void unmount() throws VolumeException {
- try {
- mount.unmount();
- } catch (Mounter.CommandFailedException e) {
- throw new VolumeException(e);
- }
- cleanup();
- onExitAction.accept(null);
- }
-
- @Override
- public synchronized void unmountForced() throws VolumeException {
- try {
- mount.forced().orElseThrow(IllegalStateException::new).unmount();
- } catch (Mounter.CommandFailedException e) {
- throw new VolumeException(e);
- }
- cleanup();
- onExitAction.accept(null);
- }
-
- @Override
- public Optional getMountPoint() {
- return mount.getMountPoint();
- }
-
- @Override
- public MountPointRequirement getMountPointRequirement() {
- return MountPointRequirement.NONE;
- }
-
- private String getLocalhostAliasOrNull() {
- return environment.getLoopbackAlias().map(alias -> {
- try {
- var address = InetAddress.getByName(alias);
- if (address.getHostAddress().equals("127.0.0.1")) {
- return alias;
- }
- } catch (UnknownHostException e) {
- //no-op
- }
- return null;
- }).orElse(null);
- }
-
- private void cleanup() {
- if (servlet != null) {
- servlet.stop();
- }
-
- }
-
- @Override
- public boolean isSupported() {
- return WebDavVolume.isSupportedStatic();
- }
-
- @Override
- public VolumeImpl getImplementationType() {
- return VolumeImpl.WEBDAV;
- }
-
- @Override
- public boolean supportsForcedUnmount() {
- return mount != null && mount.forced().isPresent();
- }
-
-
- public static boolean isSupportedStatic() {
- return true;
- }
-}
diff --git a/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java b/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java
deleted file mode 100644
index 9ca56b462..000000000
--- a/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.common.vaults;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import org.apache.commons.lang3.SystemUtils;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import java.nio.file.FileSystems;
-import java.nio.file.Path;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import java.util.stream.StreamSupport;
-
-@Singleton
-public final class WindowsDriveLetters {
-
- private static final Set A_TO_Z;
-
- static {
- try (IntStream stream = IntStream.rangeClosed('A', 'Z')) {
- A_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(ImmutableSet.toImmutableSet());
- }
- }
-
- @Inject
- public WindowsDriveLetters() {
- }
-
- public Set getAllDriveLetters() {
- return A_TO_Z;
- }
-
- public Set getOccupiedDriveLetters() {
- if (!SystemUtils.IS_OS_WINDOWS) {
- return Set.of();
- } else {
- Iterable rootDirs = FileSystems.getDefault().getRootDirectories();
- return StreamSupport.stream(rootDirs.spliterator(), false).map(p -> p.toString().substring(0, 1)).collect(Collectors.toSet());
- }
- }
-
- public Set getAvailableDriveLetters() {
- return Sets.difference(getAllDriveLetters(), getOccupiedDriveLetters());
- }
-
- public Optional getAvailableDriveLetter() {
- return getAvailableDriveLetters().stream().findFirst();
- }
-
- public Optional getAvailableDriveLetterPath() {
- return getAvailableDriveLetter().map(this::toPath);
- }
-
- /**
- * Skips A and B and only returns them if all other are occupied.
- *
- * @return an Optional containing either the letter of a free drive letter or empty, if none is available
- */
- public Optional getDesiredAvailableDriveLetter() {
- var availableDriveLetters = getAvailableDriveLetters();
- var optString = availableDriveLetters.stream().filter(s -> !(s.equals("A") || s.equals("B"))).findFirst();
- return optString.or(() -> availableDriveLetters.stream().findFirst());
- }
-
- /**
- * Skips A and B and only returns them if all other are occupied.
- *
- * @return an Optional containing either the path to a free drive letter or empty, if none is available
- */
- public Optional getDesiredAvailableDriveLetterPath() {
- return getDesiredAvailableDriveLetter().map(this::toPath);
- }
-
- public Path toPath(String driveLetter) {
- return Path.of(driveLetter + ":\\");
- }
-}
diff --git a/src/main/java/org/cryptomator/ui/common/HostServiceRevealer.java b/src/main/java/org/cryptomator/ui/common/HostServiceRevealer.java
deleted file mode 100644
index 829480592..000000000
--- a/src/main/java/org/cryptomator/ui/common/HostServiceRevealer.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.cryptomator.ui.common;
-
-import dagger.Lazy;
-import org.cryptomator.common.vaults.Volume;
-import org.cryptomator.ui.fxapp.FxApplicationScoped;
-
-import javax.inject.Inject;
-import javafx.application.Application;
-import java.nio.file.Path;
-
-@FxApplicationScoped
-public class HostServiceRevealer implements Volume.Revealer {
-
- private final Lazy application;
-
- @Inject
- public HostServiceRevealer(Lazy application) {
- this.application = application;
- }
-
- @Override
- public void reveal(Path p) throws Volume.VolumeException {
- application.get().getHostServices().showDocument(p.toUri().toString());
- }
-}
diff --git a/src/main/java/org/cryptomator/ui/common/VaultService.java b/src/main/java/org/cryptomator/ui/common/VaultService.java
index 8486a09c0..95129f6ee 100644
--- a/src/main/java/org/cryptomator/ui/common/VaultService.java
+++ b/src/main/java/org/cryptomator/ui/common/VaultService.java
@@ -1,16 +1,20 @@
package org.cryptomator.ui.common;
-import org.cryptomator.common.vaults.LockNotCompletedException;
+import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
-import org.cryptomator.common.vaults.Volume;
+import org.cryptomator.integrations.mount.Mountpoint;
+import org.cryptomator.integrations.mount.UnmountFailedException;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
+import javafx.application.Application;
+import javafx.application.HostServices;
import javafx.concurrent.Task;
import javafx.stage.Stage;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
@@ -24,13 +28,13 @@ public class VaultService {
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
+ private final Lazy application;
private final ExecutorService executorService;
- private final HostServiceRevealer vaultRevealer;
@Inject
- public VaultService(ExecutorService executorService, HostServiceRevealer vaultRevealer) {
+ public VaultService(Lazy application, ExecutorService executorService) {
+ this.application = application;
this.executorService = executorService;
- this.vaultRevealer = vaultRevealer;
}
public void reveal(Vault vault) {
@@ -43,7 +47,7 @@ public class VaultService {
* @param vault The vault to reveal
*/
public Task createRevealTask(Vault vault) {
- Task task = new RevealVaultTask(vault, vaultRevealer);
+ Task task = new RevealVaultTask(vault, application.get().getHostServices());
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName()));
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException()));
return task;
@@ -106,22 +110,21 @@ public class VaultService {
private static class RevealVaultTask extends Task {
private final Vault vault;
- private final Volume.Revealer revealer;
+ private final HostServices hostServices;
- /**
- * @param vault The vault to lock
- * @param revealer The object to use to show the vault content to the user.
- */
- public RevealVaultTask(Vault vault, Volume.Revealer revealer) {
+ public RevealVaultTask(Vault vault, HostServices hostServices) {
this.vault = vault;
- this.revealer = revealer;
-
+ this.hostServices = hostServices;
setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException()));
}
@Override
- protected Vault call() throws Volume.VolumeException {
- vault.reveal(revealer);
+ protected Vault call() {
+ switch (vault.getMountPoint()) {
+ case null -> LOG.warn("Not currently mounted");
+ case Mountpoint.WithPath m -> hostServices.showDocument(m.uri().toString());
+ case Mountpoint.WithUri m -> LOG.info("Vault mounted at {}", m.uri()); // TODO show in UI?
+ }
return vault;
}
}
@@ -180,7 +183,7 @@ public class VaultService {
}
@Override
- protected Vault call() throws Volume.VolumeException, LockNotCompletedException {
+ protected Vault call() throws UnmountFailedException, IOException {
vault.lock(forced);
return vault;
}
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java
index 569a3674a..f19cb3ed4 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java
@@ -3,10 +3,9 @@ package org.cryptomator.ui.fxapp;
import com.google.common.base.Preconditions;
import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.settings.Settings;
-import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
-import org.cryptomator.common.vaults.Volume;
+import org.cryptomator.integrations.mount.UnmountFailedException;
import org.cryptomator.ui.common.VaultService;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
@@ -18,6 +17,7 @@ import javafx.collections.ObservableList;
import java.awt.Desktop;
import java.awt.desktop.QuitResponse;
import java.awt.desktop.QuitStrategy;
+import java.io.IOException;
import java.util.EnumSet;
import java.util.EventObject;
import java.util.Set;
@@ -128,10 +128,8 @@ public class FxApplicationTerminator {
if (vault.isUnlocked()) {
try {
vault.lock(true);
- } catch (Volume.VolumeException e) {
+ } catch (UnmountFailedException | IOException e) {
LOG.error("Failed to unmount vault " + vault.getPath(), e);
- } catch (LockNotCompletedException e) {
- LOG.error("Failed to lock vault " + vault.getPath(), e);
}
}
}
diff --git a/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java b/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java
index 2d4961dde..9f24b2f83 100644
--- a/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java
+++ b/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java
@@ -1,10 +1,9 @@
package org.cryptomator.ui.lock;
import dagger.Lazy;
-import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
-import org.cryptomator.common.vaults.Volume;
+import org.cryptomator.integrations.mount.UnmountFailedException;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
@@ -17,6 +16,7 @@ import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
+import java.io.IOException;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -53,21 +53,21 @@ public class LockWorkflow extends Task {
}
@Override
- protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException, ExecutionException {
+ protected Void call() throws InterruptedException, ExecutionException, IOException {
lock(false);
return null;
}
- private void lock(boolean forced) throws InterruptedException, ExecutionException {
+ private void lock(boolean forced) throws InterruptedException, ExecutionException, IOException { //TODO: catch or rethrow IOException?
try {
vault.lock(forced);
- } catch (Volume.VolumeException | LockNotCompletedException e) {
+ } catch (UnmountFailedException e) {
LOG.info("Locking {} failed (forced: {}).", vault.getDisplayName(), forced, e);
retryOrCancel();
}
}
- private void retryOrCancel() throws ExecutionException, InterruptedException {
+ private void retryOrCancel() throws ExecutionException, InterruptedException, IOException {
try {
boolean forced = askWhetherToUseTheForce().get();
lock(forced);
@@ -105,7 +105,7 @@ public class LockWorkflow extends Task {
final var throwable = super.getException();
LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
- if (throwable instanceof Volume.VolumeException) {
+ if (throwable instanceof UnmountFailedException) { //TODO: check if correct exception caught
lockWindow.setScene(lockFailedScene.get());
lockWindow.show();
} else {
diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java
index 4bbfcce19..1e5185c06 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java
@@ -1,10 +1,12 @@
package org.cryptomator.ui.mainwindow;
+import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.integrations.revealpath.RevealFailedException;
import org.cryptomator.integrations.revealpath.RevealPathService;
import org.cryptomator.ui.common.FxController;
@@ -21,9 +23,11 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.input.Clipboard;
+import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
@@ -44,6 +48,7 @@ public class VaultDetailUnlockedController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultDetailUnlockedController.class);
private static final String ACTIVE_CLASS = "active";
+
private final ReadOnlyObjectProperty vault;
private final FxApplicationWindows appWindows;
private final VaultService vaultService;
@@ -52,9 +57,13 @@ public class VaultDetailUnlockedController implements FxController {
private final ResourceBundle resourceBundle;
private final LoadingCache vaultStats;
private final VaultStatisticsComponent.Builder vaultStatsBuilder;
+ private final ObservableValue accessibleViaPath;
+ private final ObservableValue accessibleViaUri;
+ private final ObservableValue mountPoint;
private final BooleanProperty draggingOver = new SimpleBooleanProperty();
private final BooleanProperty ciphertextPathsCopied = new SimpleBooleanProperty();
+ //FXML
public Button dropZone;
@Inject
@@ -67,6 +76,16 @@ public class VaultDetailUnlockedController implements FxController {
this.resourceBundle = resourceBundle;
this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
this.vaultStatsBuilder = vaultStatsBuilder;
+ var mp = vault.flatMap(Vault::mountPointProperty);
+ this.accessibleViaPath = mp.map(m -> m instanceof Mountpoint.WithPath).orElse(false);
+ this.accessibleViaUri = mp.map(m -> m instanceof Mountpoint.WithUri).orElse(false);
+ this.mountPoint = mp.map(m -> {
+ if (m instanceof Mountpoint.WithPath mwp) {
+ return mwp.path().toString();
+ } else {
+ return m.uri().toASCIIString();
+ }
+ });
}
public void initialize() {
@@ -105,6 +124,13 @@ public class VaultDetailUnlockedController implements FxController {
vaultService.reveal(vault.get());
}
+ @FXML
+ public void copyMountUri() {
+ ClipboardContent clipboardContent = new ClipboardContent();
+ clipboardContent.putString(mountPoint.getValue());
+ Clipboard.getSystemClipboard().setContent(clipboardContent);
+ }
+
@FXML
public void lock() {
appWindows.startLockWorkflow(vault.get(), mainWindow);
@@ -117,9 +143,10 @@ public class VaultDetailUnlockedController implements FxController {
@FXML
public void chooseFileAndReveal() {
+ Preconditions.checkState(accessibleViaPath.getValue());
var fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.filePickerTitle"));
- fileChooser.setInitialDirectory(Path.of(vault.get().getAccessPoint()).toFile());
+ fileChooser.setInitialDirectory(Path.of(mountPoint.getValue()).toFile());
var cleartextFile = fileChooser.showOpenDialog(mainWindow);
if (cleartextFile != null) {
var ciphertextPaths = getCiphertextPath(cleartextFile.toPath()).stream().toList();
@@ -128,7 +155,7 @@ public class VaultDetailUnlockedController implements FxController {
}
private boolean startsWithVaultAccessPoint(Path path) {
- return path.startsWith(vault.get().getAccessPoint());
+ return path.startsWith(Path.of(mountPoint.getValue()));
}
private Optional getCiphertextPath(Path path) {
@@ -137,7 +164,7 @@ public class VaultDetailUnlockedController implements FxController {
return Optional.empty();
}
try {
- var accessPoint = vault.get().getAccessPoint();
+ var accessPoint = mountPoint.getValue();
var cleartextPath = path.toString().substring(accessPoint.length());
if (!cleartextPath.startsWith("/")) {
cleartextPath = "/" + cleartextPath;
@@ -195,6 +222,30 @@ public class VaultDetailUnlockedController implements FxController {
return vault.get();
}
+ public ObservableValue accessibleViaPathProperty() {
+ return accessibleViaPath;
+ }
+
+ public boolean isAccessibleViaPath() {
+ return accessibleViaPath.getValue();
+ }
+
+ public ObservableValue accessibleViaUriProperty() {
+ return accessibleViaUri;
+ }
+
+ public boolean isAccessibleViaUri() {
+ return accessibleViaUri.getValue();
+ }
+
+ public ObservableValue mountPointProperty() {
+ return mountPoint;
+ }
+
+ public String getMountPoint() {
+ return mountPoint.getValue();
+ }
+
public BooleanProperty ciphertextPathsCopiedProperty() {
return ciphertextPathsCopied;
}
diff --git a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java
index 4af86dc85..00e680d2d 100644
--- a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java
+++ b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java
@@ -1,62 +1,73 @@
package org.cryptomator.ui.preferences;
-import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.ObservableUtil;
import org.cryptomator.common.settings.Settings;
-import org.cryptomator.common.settings.VolumeImpl;
-import org.cryptomator.common.settings.WebDavUrlScheme;
-import org.cryptomator.common.vaults.Volume;
+import org.cryptomator.integrations.mount.MountCapability;
+import org.cryptomator.integrations.mount.MountService;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.binding.BooleanExpression;
+import javafx.beans.value.ObservableValue;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextField;
import javafx.util.StringConverter;
+import java.util.List;
+import java.util.Optional;
+import java.util.ResourceBundle;
-/**
- * TODO: if WebDAV is selected under Windows, show warning that specific mount options (like selecting a directory as mount point) are _not_ supported
- */
@PreferencesScoped
public class VolumePreferencesController implements FxController {
private final Settings settings;
- private final BooleanBinding showWebDavSettings;
- private final BooleanBinding showWebDavScheme;
- public ChoiceBox volumeTypeChoiceBox;
- public TextField webDavPortField;
- public Button changeWebDavPortButton;
- public ChoiceBox webDavUrlSchemeChoiceBox;
+ private final ObservableValue selectedMountService;
+ private final ResourceBundle resourceBundle;
+ private final BooleanExpression loopbackPortSupported;
+ private final ObservableValue mountToDirSupported;
+ private final ObservableValue mountToDriveLetterSupported;
+ private final ObservableValue mountFlagsSupported;
+ private final ObservableValue readonlySupported;
+ private final List mountProviders;
+ public ChoiceBox volumeTypeChoiceBox;
+ public TextField loopbackPortField;
+ public Button loopbackPortApplyButton;
@Inject
- VolumePreferencesController(Settings settings) {
+ VolumePreferencesController(Settings settings, List mountProviders, ResourceBundle resourceBundle) {
this.settings = settings;
- this.showWebDavSettings = Bindings.equal(settings.preferredVolumeImpl(), VolumeImpl.WEBDAV);
- this.showWebDavScheme = showWebDavSettings.and(new SimpleBooleanProperty(SystemUtils.IS_OS_LINUX)); //TODO: remove SystemUtils
+ this.mountProviders = mountProviders;
+ this.resourceBundle = resourceBundle;
+
+ 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.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));
}
public void initialize() {
- volumeTypeChoiceBox.getItems().addAll(Volume.getCurrentSupportedAdapters());
- if (!volumeTypeChoiceBox.getItems().contains(settings.preferredVolumeImpl().get())) {
- settings.preferredVolumeImpl().set(VolumeImpl.WEBDAV);
- }
- volumeTypeChoiceBox.valueProperty().bindBidirectional(settings.preferredVolumeImpl());
- volumeTypeChoiceBox.setConverter(new VolumeImplConverter());
+ volumeTypeChoiceBox.getItems().add(null);
+ volumeTypeChoiceBox.getItems().addAll(mountProviders);
+ volumeTypeChoiceBox.setConverter(new MountServiceConverter());
+ boolean autoSelected = settings.mountService().get() == null;
+ volumeTypeChoiceBox.getSelectionModel().select(autoSelected ? null : selectedMountService.getValue());
+ volumeTypeChoiceBox.valueProperty().addListener((observableValue, oldProvider, newProvider) -> {
+ var toSet = Optional.ofNullable(newProvider).map(nP -> nP.getClass().getName()).orElse(null);
+ settings.mountService().set(toSet);
+ });
- webDavPortField.setText(String.valueOf(settings.port().get()));
- changeWebDavPortButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(webDavPortField.textProperty()));
- changeWebDavPortButton.disableProperty().bind(Bindings.createBooleanBinding(this::validateWebDavPort, webDavPortField.textProperty()).not());
-
- webDavUrlSchemeChoiceBox.getItems().addAll(WebDavUrlScheme.values());
- webDavUrlSchemeChoiceBox.valueProperty().bindBidirectional(settings.preferredGvfsScheme());
- webDavUrlSchemeChoiceBox.setConverter(new WebDavUrlSchemeConverter());
+ loopbackPortField.setText(String.valueOf(settings.port().get()));
+ loopbackPortApplyButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(loopbackPortField.textProperty()));
+ loopbackPortApplyButton.disableProperty().bind(Bindings.createBooleanBinding(this::validateLoopbackPort, loopbackPortField.textProperty()).not());
}
- private boolean validateWebDavPort() {
+ private boolean validateLoopbackPort() {
try {
- int port = Integer.parseInt(webDavPortField.getText());
+ int port = Integer.parseInt(loopbackPortField.getText());
return port == 0 // choose port automatically
|| port >= Settings.MIN_PORT && port <= Settings.MAX_PORT; // port within range
} catch (NumberFormatException e) {
@@ -64,54 +75,70 @@ public class VolumePreferencesController implements FxController {
}
}
- public void doChangeWebDavPort() {
- settings.port().set(Integer.parseInt(webDavPortField.getText()));
+ public void doChangeLoopbackPort() {
+ if (validateLoopbackPort()) {
+ settings.port().set(Integer.parseInt(loopbackPortField.getText()));
+ }
}
/* Property Getters */
- public BooleanBinding showWebDavSettingsProperty() {
- return showWebDavSettings;
+ public BooleanExpression loopbackPortSupportedProperty() {
+ return loopbackPortSupported;
}
- public Boolean getShowWebDavSettings() {
- return showWebDavSettings.get();
+ public boolean isLoopbackPortSupported() {
+ return loopbackPortSupported.get();
}
- public BooleanBinding showWebDavSchemeProperty() {
- return showWebDavScheme;
+ public ObservableValue readonlySupportedProperty() {
+ return readonlySupported;
}
- public Boolean getShowWebDavScheme() {
- return showWebDavScheme.get();
+ public boolean isReadonlySupported() {
+ return readonlySupported.getValue();
}
- /* Helper classes */
+ public ObservableValue mountToDirSupportedProperty() {
+ return mountToDirSupported;
+ }
- private static class WebDavUrlSchemeConverter extends StringConverter {
+ public boolean isMountToDirSupported() {
+ return mountToDirSupported.getValue();
+ }
+
+ public ObservableValue mountToDriveLetterSupportedProperty() {
+ return mountToDriveLetterSupported;
+ }
+
+ public boolean isMountToDriveLetterSupported() {
+ return mountToDriveLetterSupported.getValue();
+ }
+
+ public ObservableValue mountFlagsSupportedProperty() {
+ return mountFlagsSupported;
+ }
+
+ public boolean isMountFlagsSupported() {
+ return mountFlagsSupported.getValue();
+ }
+
+ /* Helpers */
+
+ private class MountServiceConverter extends StringConverter {
@Override
- public String toString(WebDavUrlScheme scheme) {
- return scheme.getDisplayName();
+ public String toString(MountService provider) {
+ if (provider == null) {
+ return resourceBundle.getString("preferences.volume.type.automatic");
+ } else {
+ return provider.displayName();
+ }
}
@Override
- public WebDavUrlScheme fromString(String string) {
+ public MountService fromString(String string) {
throw new UnsupportedOperationException();
}
}
-
- private static class VolumeImplConverter extends StringConverter {
-
- @Override
- public String toString(VolumeImpl impl) {
- return impl.getDisplayName();
- }
-
- @Override
- public VolumeImpl fromString(String string) {
- throw new UnsupportedOperationException();
- }
- }
-
}
diff --git a/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java b/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java
index 14855cbe9..704ffa9ab 100644
--- a/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java
+++ b/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java
@@ -71,7 +71,6 @@ public class AwtTrayMenuController implements TrayMenuController {
addChildren(menu, items);
}
-
@Override
public void onBeforeOpenMenu(Runnable listener) {
Preconditions.checkNotNull(this.trayIcon);
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java b/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java
index 234bac65b..aeb4a9166 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java
@@ -1,13 +1,18 @@
package org.cryptomator.ui.unlock;
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.vaults.MountPointRequirement;
+import org.cryptomator.common.mount.MountPointNotExistsException;
+import org.cryptomator.common.mount.MountPointNotSupportedException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.controls.FormattedLabel;
+import org.cryptomator.ui.fxapp.FxApplicationWindows;
+import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
+import java.util.ResourceBundle;
+import java.util.concurrent.atomic.AtomicReference;
//At the current point in time only the CustomMountPointChooser may cause this window to be shown.
@UnlockScoped
@@ -15,11 +20,32 @@ public class UnlockInvalidMountPointController implements FxController {
private final Stage window;
private final Vault vault;
+ private final AtomicReference unlockException;
+ private final FxApplicationWindows appWindows;
+ private final ResourceBundle resourceBundle;
+
+ public FormattedLabel dialogDescription;
@Inject
- UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault) {
+ UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault, @UnlockWindow AtomicReference unlockException, FxApplicationWindows appWindows, ResourceBundle resourceBundle) {
this.window = window;
this.vault = vault;
+ this.unlockException = unlockException;
+ this.appWindows = appWindows;
+ this.resourceBundle = resourceBundle;
+ }
+
+ @FXML
+ public void initialize() {
+ var e = unlockException.get();
+ String translationKey = "unlock.error.customPath.description.generic";
+ if (e instanceof MountPointNotSupportedException) {
+ translationKey = "unlock.error.customPath.description.notSupported";
+ } else if (e instanceof MountPointNotExistsException) {
+ translationKey = "unlock.error.customPath.description.notExists";
+ }
+ dialogDescription.setFormat(resourceBundle.getString(translationKey));
+ dialogDescription.setArg1(e.getMessage());
}
@FXML
@@ -27,30 +53,10 @@ public class UnlockInvalidMountPointController implements FxController {
window.close();
}
- /* Getter/Setter */
-
- public String getMountPoint() {
- return vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
+ @FXML
+ public void closeAndOpenPreferences() {
+ appWindows.showPreferencesWindow(SelectedPreferencesTab.VOLUME);
+ window.close();
}
- public boolean getNotExisting() {
- return getMountPointRequirement() == MountPointRequirement.EMPTY_MOUNT_POINT;
- }
-
- public boolean getExisting() {
- return getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT;
- }
-
- public boolean getDriveLetterOccupied() {
- return getMountPointRequirement() == MountPointRequirement.UNUSED_ROOT_DIR;
- }
-
- private MountPointRequirement getMountPointRequirement() {
- var requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!")).getMountPointRequirement();
- assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
- assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
- assert requirement != MountPointRequirement.UNUSED_ROOT_DIR || SystemUtils.IS_OS_WINDOWS; //Not implemented anywhere, but on Windows
-
- return requirement;
- }
}
\ 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 b14286698..b59fa272d 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
@@ -23,6 +23,7 @@ import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Map;
import java.util.ResourceBundle;
+import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {KeyLoadingComponent.class})
abstract class UnlockModule {
@@ -57,6 +58,13 @@ abstract class UnlockModule {
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
}
+ @Provides
+ @UnlockWindow
+ @UnlockScoped
+ static AtomicReference unlockException() {
+ return new AtomicReference<>();
+ }
+
@Provides
@FxmlScene(FxmlFile.UNLOCK_SUCCESS)
@UnlockScoped
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
index 45b9bf000..985f1b471 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
@@ -2,13 +2,11 @@ package org.cryptomator.ui.unlock;
import com.google.common.base.Throwables;
import dagger.Lazy;
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.mountpoint.InvalidMountPointException;
-import org.cryptomator.common.vaults.MountPointRequirement;
+import org.cryptomator.common.mount.IllegalMountPointException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
-import org.cryptomator.common.vaults.Volume.VolumeException;
import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.integrations.mount.MountFailedException;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.VaultService;
@@ -23,9 +21,7 @@ import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
-import java.nio.file.DirectoryNotEmptyException;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.NotDirectoryException;
+import java.util.concurrent.atomic.AtomicReference;
/**
* A multi-step task that consists of background activities as well as user interaction.
@@ -44,9 +40,10 @@ public class UnlockWorkflow extends Task {
private final Lazy invalidMountPointScene;
private final FxApplicationWindows appWindows;
private final KeyLoadingStrategy keyLoadingStrategy;
+ private final AtomicReference unlockFailedException;
@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) {
+ 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 AtomicReference unlockFailedException) {
this.window = window;
this.vault = vault;
this.vaultService = vaultService;
@@ -54,10 +51,11 @@ public class UnlockWorkflow extends Task {
this.invalidMountPointScene = invalidMountPointScene;
this.appWindows = appWindows;
this.keyLoadingStrategy = keyLoadingStrategy;
+ this.unlockFailedException = unlockFailedException;
}
@Override
- protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
+ protected Boolean call() throws InterruptedException, IOException, CryptoException, MountFailedException {
try {
attemptUnlock();
return true;
@@ -67,50 +65,21 @@ public class UnlockWorkflow extends Task {
}
}
- private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
+ private void attemptUnlock() throws IOException, CryptoException, MountFailedException {
try {
keyLoadingStrategy.use(vault::unlock);
} catch (Exception e) {
Throwables.propagateIfPossible(e, IOException.class);
- Throwables.propagateIfPossible(e, VolumeException.class);
- Throwables.propagateIfPossible(e, InvalidMountPointException.class);
Throwables.propagateIfPossible(e, CryptoException.class);
+ Throwables.propagateIfPossible(e, IllegalMountPointException.class);
+ Throwables.propagateIfPossible(e, MountFailedException.class);
throw new IllegalStateException("unexpected exception type", e);
}
}
- private void handleInvalidMountPoint(InvalidMountPointException impExc) {
- var requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!", impExc)).getMountPointRequirement();
- assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
- assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
- assert requirement != MountPointRequirement.UNUSED_ROOT_DIR || SystemUtils.IS_OS_WINDOWS; //Not implemented anywhere, but on Windows
-
- Throwable cause = impExc.getCause();
- // TODO: apply https://openjdk.java.net/jeps/8213076 in future JDK versions
- if (cause instanceof NotDirectoryException) {
- if (requirement == MountPointRequirement.PARENT_NO_MOUNT_POINT) {
- LOG.error("Unlock failed. Parent folder is missing: {}", cause.getMessage());
- } else {
- LOG.error("Unlock failed. Mountpoint doesn't exist (needs to be a folder): {}", cause.getMessage());
- }
- showInvalidMountPointScene();
- } else if (cause instanceof FileAlreadyExistsException) {
- if (requirement == MountPointRequirement.UNUSED_ROOT_DIR) {
- LOG.error("Unlock failed. Drive Letter already in use: {}", cause.getMessage());
- } else {
- LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
- }
- showInvalidMountPointScene();
- } else if (cause instanceof DirectoryNotEmptyException) {
- LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());
- showInvalidMountPointScene();
- } else {
- handleGenericError(impExc);
- }
- }
-
- private void showInvalidMountPointScene() {
+ private void handleIllegalMountPointError(IllegalMountPointException impe) {
Platform.runLater(() -> {
+ unlockFailedException.set(impe);
window.setScene(invalidMountPointScene.get());
window.show();
});
@@ -144,8 +113,8 @@ public class UnlockWorkflow extends Task {
protected void failed() {
LOG.info("Unlock of '{}' failed.", vault.getDisplayName());
Throwable throwable = super.getException();
- if (throwable instanceof InvalidMountPointException e) {
- handleInvalidMountPoint(e);
+ if(throwable instanceof IllegalMountPointException impe) {
+ handleIllegalMountPointError(impe);
} else {
handleGenericError(throwable);
}
diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
index b7b01fdd6..502d4312a 100644
--- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
+++ b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
@@ -1,17 +1,16 @@
package org.cryptomator.ui.vaultoptions;
import com.google.common.base.Strings;
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.Environment;
-import org.cryptomator.common.settings.Settings;
-import org.cryptomator.common.settings.VolumeImpl;
+import org.cryptomator.common.mount.ActualMountService;
+import org.cryptomator.common.mount.WindowsDriveLetters;
+import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.common.vaults.WindowsDriveLetters;
+import org.cryptomator.integrations.mount.MountCapability;
import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.fxapp.FxApplicationWindows;
+import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import javax.inject.Inject;
-import javafx.beans.binding.Bindings;
-import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
@@ -34,98 +33,128 @@ import java.util.Set;
public class MountOptionsController implements FxController {
private final Stage window;
- private final Vault vault;
- private final VolumeImpl usedVolumeImpl;
+ private final VaultSettings vaultSettings;
private final WindowsDriveLetters windowsDriveLetters;
private final ResourceBundle resourceBundle;
+ private final ObservableValue defaultMountFlags;
+ private final ObservableValue mountpointDirSupported;
+ private final ObservableValue mountpointDriveLetterSupported;
+ private final ObservableValue readOnlySupported;
+ private final ObservableValue mountFlagsSupported;
+ private final ObservableValue directoryPath;
+ private final FxApplicationWindows applicationWindows;
+
+
+ //-- FXML objects --
public CheckBox readOnlyCheckbox;
public CheckBox customMountFlagsCheckbox;
- public TextField mountFlags;
- public ToggleGroup mountPoint;
- public RadioButton mountPointAuto;
- public RadioButton mountPointWinDriveLetter;
- public RadioButton mountPointCustomDir;
- public ChoiceBox driveLetterSelection;
+ public TextField mountFlagsField;
+ public ToggleGroup mountPointToggleGroup;
+ public RadioButton mountPointAutoBtn;
+ public RadioButton mountPointDriveLetterBtn;
+ public RadioButton mountPointDirBtn;
+ public TextField directoryPathField;
+ public ChoiceBox driveLetterSelection;
@Inject
- MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, Settings settings, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, Environment environment) {
+ MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue mountService, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, FxApplicationWindows applicationWindows) {
this.window = window;
- this.vault = vault;
- this.usedVolumeImpl = settings.preferredVolumeImpl().get();
+ 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();
+ } 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;
}
@FXML
public void initialize() {
-
// readonly:
- readOnlyCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().usesReadOnlyMode());
- //TODO: support this feature on Windows
- if (usedVolumeImpl == VolumeImpl.FUSE && isOsWindows()) {
- readOnlyCheckbox.setSelected(false); // to prevent invalid states
- readOnlyCheckbox.setDisable(true);
- }
+ readOnlyCheckbox.selectedProperty().bindBidirectional(vaultSettings.usesReadOnlyMode());
// custom mount flags:
- mountFlags.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().not());
- customMountFlagsCheckbox.setSelected(vault.isHavingCustomMountFlags());
- if (vault.isHavingCustomMountFlags()) {
- mountFlags.textProperty().bindBidirectional(vault.getVaultSettings().mountFlags());
- readOnlyCheckbox.setSelected(false); // to prevent invalid states
- } else {
- mountFlags.textProperty().bind(vault.defaultMountFlagsProperty());
- }
+ mountFlagsField.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().not());
+ customMountFlagsCheckbox.setSelected(!Strings.isNullOrEmpty(vaultSettings.mountFlags().getValue()));
+ toggleUseCustomMountFlags();
- // mount point options:
- mountPoint.selectedToggleProperty().addListener(this::toggleMountPoint);
- driveLetterSelection.getItems().addAll(windowsDriveLetters.getAllDriveLetters());
+ //driveLetter choice box
+ driveLetterSelection.getItems().addAll(windowsDriveLetters.getAll());
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle));
- driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get());
- if (vault.getVaultSettings().useCustomMountPath().get() && vault.getVaultSettings().getCustomMountPath().isPresent()) {
- mountPoint.selectToggle(mountPointCustomDir);
- } else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) {
- mountPoint.selectToggle(mountPointWinDriveLetter);
+ //mountPoint toggle group
+ var mountPoint = vaultSettings.getMountPoint();
+ if (mountPoint == null) {
+ //prepare and select auto
+ mountPointToggleGroup.selectToggle(mountPointAutoBtn);
+ } else if (mountPoint.getParent() == null && isDriveLetter(mountPoint)) {
+ //prepare and select drive letter
+ mountPointToggleGroup.selectToggle(mountPointDriveLetterBtn);
+ driveLetterSelection.valueProperty().bindBidirectional(vaultSettings.mountPoint());
} else {
- mountPoint.selectToggle(mountPointAuto);
+ //prepare and select dir
+ mountPointToggleGroup.selectToggle(mountPointDirBtn);
}
+ mountPointToggleGroup.selectedToggleProperty().addListener(this::selectedToggleChanged);
+ }
- vault.getVaultSettings().useCustomMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
- vault.getVaultSettings().winDriveLetter().bind( //
- Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) //
- .then(driveLetterSelection.getSelectionModel().selectedItemProperty()) //
- .otherwise((String) null) //
- );
+ @FXML
+ public void openVolumePreferences() {
+ applicationWindows.showPreferencesWindow(SelectedPreferencesTab.VOLUME);
}
@FXML
public void toggleUseCustomMountFlags() {
if (customMountFlagsCheckbox.isSelected()) {
readOnlyCheckbox.setSelected(false); // to prevent invalid states
- mountFlags.textProperty().unbind();
- vault.setCustomMountFlags(vault.defaultMountFlagsProperty().get());
- mountFlags.textProperty().bindBidirectional(vault.getVaultSettings().mountFlags());
+ mountFlagsField.textProperty().unbind();
+ var mountFlags = vaultSettings.mountFlags().get();
+ if (mountFlags == null || mountFlags.isBlank()) {
+ vaultSettings.mountFlags().set(defaultMountFlags.getValue());
+ }
+ mountFlagsField.textProperty().bindBidirectional(vaultSettings.mountFlags());
} else {
- mountFlags.textProperty().unbindBidirectional(vault.getVaultSettings().mountFlags());
- vault.setCustomMountFlags(null);
- mountFlags.textProperty().bind(vault.defaultMountFlagsProperty());
+ mountFlagsField.textProperty().unbindBidirectional(vaultSettings.mountFlags());
+ vaultSettings.mountFlags().set(null);
+ mountFlagsField.textProperty().bind(defaultMountFlags);
}
}
@FXML
public void chooseCustomMountPoint() {
- chooseCustomMountPointOrReset(mountPointCustomDir);
+ try {
+ Path chosenPath = chooseCustomMountPointInternal();
+ vaultSettings.mountPoint().set(chosenPath);
+ } catch (NoDirSelectedException e) {
+ //no-op
+ }
}
- private void chooseCustomMountPointOrReset(Toggle previousMountToggle) {
+ /**
+ * Prepares and opens a directory chooser dialog.
+ * This method blocks until the dialog is closed.
+ *
+ * @return the absolute path to the chosen directory
+ * @throws NoDirSelectedException if dialog is closed without choosing a directory
+ */
+ private Path chooseCustomMountPointInternal() throws NoDirSelectedException {
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle"));
try {
- var initialDir = Path.of(vault.getVaultSettings().getCustomMountPath().orElse(System.getProperty("user.home")));
+ var mp = vaultSettings.mountPoint().get();
+ var initialDir = mp != null && !isDriveLetter(mp) ? mp : Path.of(System.getProperty("user.home"));
- if(Files.exists(initialDir)) {
+ if (Files.isDirectory(initialDir)) {
directoryChooser.setInitialDirectory(initialDir.toFile());
}
} catch (InvalidPathException e) {
@@ -133,73 +162,116 @@ public class MountOptionsController implements FxController {
}
File file = directoryChooser.showDialog(window);
if (file != null) {
- vault.getVaultSettings().customMountPath().set(file.getAbsolutePath());
+ return file.toPath();
} else {
- mountPoint.selectToggle(previousMountToggle);
+ throw new NoDirSelectedException();
}
}
- private void toggleMountPoint(@SuppressWarnings("unused") ObservableValue extends Toggle> observable, Toggle oldValue, Toggle newValue) {
- if (mountPointCustomDir.equals(newValue) && Strings.isNullOrEmpty(vault.getVaultSettings().customMountPath().get())) {
- chooseCustomMountPointOrReset(oldValue);
+ private void selectedToggleChanged(ObservableValue extends Toggle> observable, Toggle oldToggle, Toggle newToggle) {
+ //Remark: the mountpoint corresponding to the newToggle must be null, otherwise it would not be new!
+ driveLetterSelection.valueProperty().unbindBidirectional(vaultSettings.mountPoint());
+ if (mountPointDriveLetterBtn.equals(newToggle)) {
+ vaultSettings.mountPoint().set(windowsDriveLetters.getFirstDesiredAvailable().orElse(windowsDriveLetters.getAll().stream().findAny().get()));
+ driveLetterSelection.valueProperty().bindBidirectional(vaultSettings.mountPoint());
+ } else if (mountPointDirBtn.equals(newToggle)) {
+ try {
+ vaultSettings.mountPoint().set(chooseCustomMountPointInternal());
+ } catch (NoDirSelectedException e) {
+ if (oldToggle != null && !mountPointDirBtn.equals(oldToggle)) {
+ mountPointToggleGroup.selectToggle(oldToggle);
+ } else {
+ mountPointToggleGroup.selectToggle(mountPointAutoBtn);
+ }
+ }
+ } else {
+ vaultSettings.mountPoint().set(null);
}
}
- /**
- * Converts 'C' to "C:" to translate between model and GUI.
- */
- private static class WinDriveLetterLabelConverter extends StringConverter {
+ private boolean isDriveLetter(Path mountPoint) {
+ if (mountPoint != null) {
+ var s = mountPoint.toString();
+ return s.length() == 3 && s.endsWith(":\\");
+ }
+ return false;
+ }
- private final Set occupiedDriveLetters;
+ private static class WinDriveLetterLabelConverter extends StringConverter {
+
+ private final Set occupiedDriveLetters;
private final ResourceBundle resourceBundle;
WinDriveLetterLabelConverter(WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle) {
- this.occupiedDriveLetters = windowsDriveLetters.getOccupiedDriveLetters();
+ this.occupiedDriveLetters = windowsDriveLetters.getOccupied();
this.resourceBundle = resourceBundle;
}
@Override
- public String toString(String driveLetter) {
- if (Strings.isNullOrEmpty(driveLetter)) {
+ public String toString(Path driveLetter) {
+ if (driveLetter == null) {
return "";
} else if (occupiedDriveLetters.contains(driveLetter)) {
- return driveLetter + ": (" + resourceBundle.getString("vaultOptions.mount.winDriveLetterOccupied") + ")";
+ return driveLetter.toString().substring(0, 2) + " (" + resourceBundle.getString("vaultOptions.mount.winDriveLetterOccupied") + ")";
} else {
- return driveLetter + ":";
+ return driveLetter.toString().substring(0, 2);
}
}
@Override
- public String fromString(String string) {
- throw new UnsupportedOperationException();
+ public Path fromString(String string) {
+ if (string.isEmpty()) {
+ return null;
+ } else {
+ return Path.of(string + "\\");
+ }
}
}
+ //@formatter:off
+ private static class NoDirSelectedException extends Exception {}
+ //@formatter:on
+
// Getter & Setter
- public boolean isOsWindows() {
- return SystemUtils.IS_OS_WINDOWS;
+ public ObservableValue mountFlagsSupportedProperty() {
+ return mountFlagsSupported;
}
- public boolean isCustomMountPointSupported() {
- return !(usedVolumeImpl == VolumeImpl.WEBDAV && isOsWindows());
+ public boolean isMountFlagsSupported() {
+ return mountFlagsSupported.getValue();
+ }
+
+ public ObservableValue mountpointDirSupportedProperty() {
+ return mountpointDirSupported;
+ }
+
+ public boolean isMountpointDirSupported() {
+ return mountpointDirSupported.getValue();
+ }
+
+ public ObservableValue mountpointDriveLetterSupportedProperty() {
+ return mountpointDriveLetterSupported;
+ }
+
+ public boolean isMountpointDriveLetterSupported() {
+ return mountpointDriveLetterSupported.getValue();
+ }
+
+ public ObservableValue readOnlySupportedProperty() {
+ return readOnlySupported;
}
public boolean isReadOnlySupported() {
- return !(usedVolumeImpl == VolumeImpl.FUSE && isOsWindows());
+ return readOnlySupported.getValue();
}
- public StringProperty customMountPathProperty() {
- return vault.getVaultSettings().customMountPath();
+ public ObservableValue directoryPathProperty() {
+ return directoryPath;
}
- public boolean isCustomMountOptionsSupported() {
- return usedVolumeImpl != VolumeImpl.WEBDAV;
+ public String getDirectoryPath() {
+ return directoryPath.getValue();
}
-
- public String getCustomMountPath() {
- return vault.getVaultSettings().customMountPath().get();
- }
-
}
diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css
index fe510d420..5f0877842 100644
--- a/src/main/resources/css/dark_theme.css
+++ b/src/main/resources/css/dark_theme.css
@@ -911,3 +911,14 @@
-fx-border-style: solid inside;
-fx-border-width: 1px;
}
+
+/*******************************************************************************
+ * *
+ * Separator *
+ * *
+ ******************************************************************************/
+
+.separator {
+ -fx-padding: 0.5px;
+ -fx-background-color: CONTROL_BORDER_NORMAL;
+}
diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css
index 4e47af6df..decf64b64 100644
--- a/src/main/resources/css/light_theme.css
+++ b/src/main/resources/css/light_theme.css
@@ -910,3 +910,14 @@
-fx-border-style: solid inside;
-fx-border-width: 1px;
}
+
+/*******************************************************************************
+ * *
+ * Separator *
+ * *
+ ******************************************************************************/
+
+.separator {
+ -fx-padding: 0.5px;
+ -fx-background-color: CONTROL_BORDER_NORMAL;
+}
diff --git a/src/main/resources/fxml/preferences_volume.fxml b/src/main/resources/fxml/preferences_volume.fxml
index 2c88cf5de..92df77e3d 100644
--- a/src/main/resources/fxml/preferences_volume.fxml
+++ b/src/main/resources/fxml/preferences_volume.fxml
@@ -1,10 +1,12 @@
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/fxml/unlock_invalid_mount_point.fxml b/src/main/resources/fxml/unlock_invalid_mount_point.fxml
index 69d9c62e1..1b52f568c 100644
--- a/src/main/resources/fxml/unlock_invalid_mount_point.fxml
+++ b/src/main/resources/fxml/unlock_invalid_mount_point.fxml
@@ -34,19 +34,19 @@
-
diff --git a/src/main/resources/fxml/vault_detail_unlocked.fxml b/src/main/resources/fxml/vault_detail_unlocked.fxml
index 185bbefa8..c035b2d88 100644
--- a/src/main/resources/fxml/vault_detail_unlocked.fxml
+++ b/src/main/resources/fxml/vault_detail_unlocked.fxml
@@ -2,30 +2,38 @@
-
+
+
-