diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 9ced33c15..a82e89ba7 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -26,6 +26,7 @@ body: Examples: - Operating System: Windows 10 - Cryptomator: 1.5.16 + - OneDrive: 23.226 - LibreOffice: 7.1.4 value: | - Operating System: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e15900880..3d2f42f8d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,11 +6,38 @@ updates: interval: "weekly" day: "monday" time: "06:00" - timezone: "UTC" + timezone: "Etc/UTC" groups: - maven-dependencies: + java-test-dependencies: + patterns: + - "org.junit.jupiter:*" + - "org.mockito:*" + - "org.hamcrest:*" + - "com.google.jimfs:jimfs" + maven-build-plugins: + patterns: + - "org.apache.maven.plugins:*" + - "org.jacoco:jacoco-maven-plugin" + - "org.owasp:dependency-check-maven" + - "me.fabriciorby:maven-surefire-junit5-tree-reporter" + - "org.codehaus.mojo:license-maven-plugin" + javafx: + patterns: + - "org.openjfx:*" + java-production-dependencies: patterns: - "*" + exclude-patterns: + - "org.openjfx:*" + - "org.apache.maven.plugins:*" + - "org.jacoco:jacoco-maven-plugin" + - "org.owasp:dependency-check-maven" + - "me.fabriciorby:maven-surefire-junit5-tree-reporter" + - "org.codehaus.mojo:license-maven-plugin" + - "org.junit.jupiter:*" + - "org.mockito:*" + - "org.hamcrest:*" + - "com.google.jimfs:jimfs" - package-ecosystem: "github-actions" directory: "/" # even for `.github/workflows` diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 5821a7fde..1bb3e694a 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -38,7 +38,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: ${{ env.JAVA_DIST }} java-version: ${{ env.JAVA_VERSION }} @@ -68,7 +68,7 @@ jobs: - name: Set version run : mvn versions:set -DnewVersion=${{ needs.get-version.outputs.semVerStr }} - name: Run maven - run: mvn -B clean package -Pdependency-check,linux -DskipTests + run: mvn -B clean package -Plinux -DskipTests - name: Patch target dir run: | cp LICENSE.txt target diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fbb57cbbf..dc575baca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: ${{ env.JAVA_DIST }} java-version: ${{ env.JAVA_VERSION }} @@ -36,7 +36,7 @@ jobs: mvn -B verify jacoco:report org.sonarsource.scanner.maven:sonar-maven-plugin:sonar - -Pcoverage,dependency-check + -Pcoverage -Dsonar.projectKey=cryptomator_cryptomator -Dsonar.organization=cryptomator -Dsonar.host.url=https://sonarcloud.io diff --git a/.github/workflows/check-jdk-updates.yml b/.github/workflows/check-jdk-updates.yml index 30954b9e4..b1cdca4bb 100644 --- a/.github/workflows/check-jdk-updates.yml +++ b/.github/workflows/check-jdk-updates.yml @@ -15,7 +15,7 @@ jobs: outputs: jdk-date: ${{ steps.get-data.outputs.jdk-date}} steps: - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: java-version: ${{ env.JDK_VERSION }} distribution: ${{ env.JDK_VENDOR }} @@ -32,7 +32,7 @@ jobs: jdk-date: ${{ steps.get-data.outputs.jdk-date}} jdk-version: ${{ steps.get-data.outputs.jdk-version}} steps: - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: java-version: 21 distribution: ${{ env.JDK_VENDOR }} diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index e8fd5da22..00b49f4fc 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -46,14 +46,14 @@ jobs: sudo apt-get update sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.COFFEELIBS_JDK }}=${{ env.COFFEELIBS_JDK_VERSION }} libgtk2.0-0 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: ${{ env.JAVA_DIST }} java-version: ${{ env.JAVA_VERSION }} check-latest: true cache: 'maven' - name: Run maven - run: mvn -B clean package -Pdependency-check,linux -DskipTests + run: mvn -B clean package -Plinux -DskipTests - name: Download OpenJFX jmods id: download-jmods run: | diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml new file mode 100644 index 000000000..590688b7d --- /dev/null +++ b/.github/workflows/dependency-check.yml @@ -0,0 +1,56 @@ +name: OWASP Maven Dependency Check +on: + schedule: + - cron: '0 8 * * 0' + workflow_dispatch: + + +jobs: + check-dependencies: + name: Check dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + show-progress: false + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 21 + cache: 'maven' + - name: Cache NVD DB + uses: actions/cache@v3 + with: + path: ~/.m2/repository/org/owasp/dependency-check-data/ + key: dependency-check-${{ github.run_id }} + restore-keys: | + dependency-check + env: + SEGMENT_DOWNLOAD_TIMEOUT_MINS: 5 + - name: Run org.owasp:dependency-check plugin + id: dependency-check + continue-on-error: true + run: mvn -B validate -Pdependency-check + env: + NVD_API_KEY: ${{ secrets.NVD_API_KEY }} + - name: Upload report on failure + if: steps.dependency-check.outcome == 'failure' + uses: actions/upload-artifact@v3 + with: + name: dependency-check-report + path: target/dependency-check-report.html + if-no-files-found: error + - name: Slack Notification on regular check + if: github.event_name == 'schedule' && steps.dependency-check.outcome == 'failure' + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_USERNAME: 'Cryptobot' + SLACK_ICON: false + SLACK_ICON_EMOJI: ':bot:' + SLACK_CHANNEL: 'cryptomator-desktop' + SLACK_TITLE: "Vulnerabilities in ${{ github.event.repository.name }} detected." + SLACK_MESSAGE: "Download the for more details." + SLACK_FOOTER: false + MSG_MINIMAL: true diff --git a/.github/workflows/dl-stats.yml b/.github/workflows/dl-stats.yml index dc87a2bbd..b16899520 100644 --- a/.github/workflows/dl-stats.yml +++ b/.github/workflows/dl-stats.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Get download count of latest releases id: get-stats - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const query = `query($owner:String!, $name:String!) { diff --git a/.github/workflows/error-db.yml b/.github/workflows/error-db.yml index e885af4a2..301713681 100644 --- a/.github/workflows/error-db.yml +++ b/.github/workflows/error-db.yml @@ -14,7 +14,7 @@ jobs: - name: Query Discussion Data if: github.event_name == 'discussion_comment' || github.event_name == 'discussion' && github.event.action != 'deleted' id: query-data - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const query = `query ($owner: String!, $name: String!, $discussionNumber: Int!) { diff --git a/.github/workflows/get-version.yml b/.github/workflows/get-version.yml index 1bed1cff8..ae2b60b4b 100644 --- a/.github/workflows/get-version.yml +++ b/.github/workflows/get-version.yml @@ -39,7 +39,7 @@ jobs: with: fetch-depth: 0 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: ${{ env.JAVA_DIST }} java-version: ${{ env.JAVA_VERSION }} diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index 1372fe6ce..3b4905a12 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -49,7 +49,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: ${{ env.JAVA_DIST }} java-version: ${{ env.JAVA_VERSION }} @@ -79,7 +79,7 @@ jobs: - name: Set version run : mvn versions:set -DnewVersion=${{ needs.get-version.outputs.semVerStr }} - name: Run maven - run: mvn -B clean package -Pdependency-check,mac -DskipTests + run: mvn -B clean package -Pmac -DskipTests - name: Patch target dir run: | cp LICENSE.txt target diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 931817418..2730f5c24 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -18,10 +18,10 @@ jobs: if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: ${{ env.JAVA_DIST }} java-version: ${{ env.JAVA_VERSION }} cache: 'maven' - name: Build and Test - run: xvfb-run mvn -B clean install jacoco:report -Pcoverage,dependency-check \ No newline at end of file + run: xvfb-run mvn -B clean install jacoco:report -Pcoverage \ No newline at end of file diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index 1bbfb5d1a..1bbbc3c4d 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -10,12 +10,22 @@ defaults: run: shell: bash +env: + JAVA_DIST: 'zulu' + JAVA_VERSION: 21 + jobs: - release-check-precondition: + check-preconditions: name: Validate commits pushed to release/hotfix branch to fulfill release requirements runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: ${{ env.JAVA_DIST }} + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' - id: validate-pom-version name: Validate POM version run: | @@ -37,4 +47,19 @@ jobs: if ! grep -q "" dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml; then echo "Release not set in dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml" exit 1 - fi \ No newline at end of file + fi + - name: Cache NVD DB + uses: actions/cache@v3 + with: + path: ~/.m2/repository/org/owasp/dependency-check-data/ + key: dependency-check-${{ github.run_id }} + restore-keys: | + dependency-check + env: + SEGMENT_DOWNLOAD_TIMEOUT_MINS: 5 + - name: Run org.owasp:dependency-check plugin + id: dependency-check + continue-on-error: true + run: mvn -B verify -Pdependency-check -DskipTests + env: + NVD_API_KEY: ${{ secrets.NVD_API_KEY }} \ No newline at end of file diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index dece30a8b..648b58884 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -41,7 +41,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: ${{ env.JAVA_DIST }} java-version: ${{ env.JAVA_VERSION }} @@ -73,7 +73,7 @@ jobs: - name: Set version run : mvn versions:set -DnewVersion=${{ needs.get-version.outputs.semVerStr }} - name: Run maven - run: mvn -B clean package -Pdependency-check,win -DskipTests + run: mvn -B clean package -Pwin -DskipTests - name: Patch target dir run: | cp LICENSE.txt target @@ -275,7 +275,7 @@ jobs: path: dist/win/bundle/resources - name: Strip version info from msi file name run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: ${{ env.JAVA_DIST }} java-version: ${{ env.JAVA_VERSION }} diff --git a/README.md b/README.md index ec021ab5d..560f2e4f8 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ For more information on the security details visit [cryptomator.org](https://doc ### Dependencies -* JDK 19 (e.g. temurin) +* JDK 21 (e.g. temurin, zulu) * Maven 3 ### Run Maven diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml index e28172efe..b6f05d102 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml +++ b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml @@ -66,6 +66,7 @@ + diff --git a/pom.xml b/pom.xml index 3911795c2..666aee7c3 100644 --- a/pom.xml +++ b/pom.xml @@ -33,44 +33,44 @@ org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents - 2.6.7 + 2.6.8 1.3.0 1.2.4 1.2.2 - 1.4.0-beta2 - 4.0.0-beta4 + 1.4.0 + 4.0.0 2.0.0 2.0.5 - 3.13.0 - 2.48.1 + 3.14.0 + 2.49 2.2 32.1.3-jre - 2.15.3 - 20.0.2 + 2.16.0 + 21.0.1 4.4.0 - 9.37 - 1.4.11 + 9.37.3 + 1.4.14 2.0.9 0.8.0 1.8.2 - 5.10.0 - 5.6.0 + 5.10.1 + 5.8.0 2.2 - 24.0.1 - 8.4.0 + 24.1.0 + 9.0.7 0.8.11 - 2.2.0 + 2.3.0 1.2.1 - 3.11.0 + 3.12.1 3.3.1 - 3.6.0 - 3.1.2 + 3.6.1 + 3.2.3 3.3.0 @@ -460,17 +460,19 @@ org.owasp dependency-check-maven - 24 + 24 0 true true suppression.xml + ${env.NVD_API_KEY} check + validate diff --git a/src/main/java/org/cryptomator/common/CommonsModule.java b/src/main/java/org/cryptomator/common/CommonsModule.java index 4ac07495e..a1e3c0950 100644 --- a/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/src/main/java/org/cryptomator/common/CommonsModule.java @@ -5,10 +5,8 @@ *******************************************************************************/ package org.cryptomator.common; -import com.tobiasdiez.easybind.EasyBind; import dagger.Module; import dagger.Provides; -import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.keychain.KeychainModule; import org.cryptomator.common.mount.MountModule; import org.cryptomator.common.settings.Settings; @@ -22,8 +20,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Named; import javax.inject.Singleton; -import javafx.beans.value.ObservableValue; -import java.net.InetSocketAddress; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Comparator; @@ -136,13 +132,4 @@ public abstract class CommonsModule { LOG.error("Uncaught exception in " + thread.getName(), throwable); } - @Provides - @Singleton - static ObservableValue provideServerSocketAddressBinding(Settings settings) { - return settings.port.map(port -> { - String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost"; - return InetSocketAddress.createUnresolved(host, settings.port.intValue()); - }); - } - } diff --git a/src/main/java/org/cryptomator/common/mount/ActualMountService.java b/src/main/java/org/cryptomator/common/mount/ActualMountService.java deleted file mode 100644 index a96cc8e37..000000000 --- a/src/main/java/org/cryptomator/common/mount/ActualMountService.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.cryptomator.common.mount; - -import org.cryptomator.integrations.mount.MountService; - -public record ActualMountService(MountService service, boolean isDesired) { -} diff --git a/src/main/java/org/cryptomator/common/mount/ConflictingMountServiceException.java b/src/main/java/org/cryptomator/common/mount/ConflictingMountServiceException.java new file mode 100644 index 000000000..b4d87169a --- /dev/null +++ b/src/main/java/org/cryptomator/common/mount/ConflictingMountServiceException.java @@ -0,0 +1,14 @@ +package org.cryptomator.common.mount; + +import org.cryptomator.integrations.mount.MountFailedException; + +/** + * Thrown by {@link Mounter} to indicate that the selected mount service can not be used + * due to incompatibilities with a different mount service that is already in use. + */ +public class ConflictingMountServiceException extends MountFailedException { + + public ConflictingMountServiceException(String msg) { + super(msg); + } +} diff --git a/src/main/java/org/cryptomator/common/mount/MountModule.java b/src/main/java/org/cryptomator/common/mount/MountModule.java index cbcb23e82..72872855c 100644 --- a/src/main/java/org/cryptomator/common/mount/MountModule.java +++ b/src/main/java/org/cryptomator/common/mount/MountModule.java @@ -4,21 +4,18 @@ import dagger.Module; import dagger.Provides; import org.cryptomator.common.ObservableUtil; import org.cryptomator.common.settings.Settings; -import org.cryptomator.integrations.mount.Mount; import org.cryptomator.integrations.mount.MountService; import javax.inject.Named; import javax.inject.Singleton; import javafx.beans.value.ObservableValue; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; @Module public class MountModule { - private static final AtomicReference formerSelectedMountService = new AtomicReference<>(null); - private static final List problematicFuseMountServices = List.of("org.cryptomator.frontend.fuse.mount.MacFuseMountProvider", "org.cryptomator.frontend.fuse.mount.FuseTMountProvider"); - @Provides @Singleton static List provideSupportedMountServices() { @@ -27,46 +24,18 @@ public class MountModule { @Provides @Singleton - @Named("FUPFMS") - static AtomicReference provideFirstUsedProblematicFuseMountService() { - return new AtomicReference<>(null); + static ObservableValue provideDefaultMountService(List mountProviders, Settings settings) { + var fallbackProvider = mountProviders.stream().findFirst().get(); //there should always be a mount provider, at least webDAV + return ObservableUtil.mapWithDefault(settings.mountService, // + serviceName -> mountProviders.stream().filter(s -> s.getClass().getName().equals(serviceName)).findFirst().orElse(fallbackProvider), // + fallbackProvider); } @Provides @Singleton - static ObservableValue provideMountService(Settings settings, List serviceImpls, @Named("FUPFMS") AtomicReference fupfms) { - var fallbackProvider = serviceImpls.stream().findFirst().orElse(null); - - var observableMountService = ObservableUtil.mapWithDefault(settings.mountService, // - desiredServiceImpl -> { // - var serviceFromSettings = serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(desiredServiceImpl)).findAny(); // - var targetedService = serviceFromSettings.orElse(fallbackProvider); - return applyWorkaroundForProblematicFuse(targetedService, serviceFromSettings.isPresent(), fupfms); - }, // - () -> { // - return applyWorkaroundForProblematicFuse(fallbackProvider, true, fupfms); - }); - return observableMountService; + @Named("usedMountServices") + static Set provideSetOfUsedMountServices() { + return ConcurrentHashMap.newKeySet(); } - //see https://github.com/cryptomator/cryptomator/issues/2786 - private synchronized static ActualMountService applyWorkaroundForProblematicFuse(MountService targetedService, boolean isDesired, AtomicReference firstUsedProblematicFuseMountService) { - //set the first used problematic fuse service if applicable - var targetIsProblematicFuse = isProblematicFuseService(targetedService); - if (targetIsProblematicFuse && firstUsedProblematicFuseMountService.get() == null) { - firstUsedProblematicFuseMountService.set(targetedService); - } - - //do not use the targeted mount service and fallback to former one, if the service is problematic _and_ not the first problematic one used. - if (targetIsProblematicFuse && !firstUsedProblematicFuseMountService.get().equals(targetedService)) { - return new ActualMountService(formerSelectedMountService.get(), false); - } else { - formerSelectedMountService.set(targetedService); - return new ActualMountService(targetedService, isDesired); - } - } - - public static boolean isProblematicFuseService(MountService service) { - return problematicFuseMountServices.contains(service.getClass().getName()); - } -} +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/common/mount/Mounter.java b/src/main/java/org/cryptomator/common/mount/Mounter.java index 101524ea3..b63a12b1f 100644 --- a/src/main/java/org/cryptomator/common/mount/Mounter.java +++ b/src/main/java/org/cryptomator/common/mount/Mounter.java @@ -9,11 +9,15 @@ import org.cryptomator.integrations.mount.MountFailedException; import org.cryptomator.integrations.mount.MountService; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Singleton; import javafx.beans.value.ObservableValue; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER; import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR; @@ -24,24 +28,39 @@ import static org.cryptomator.integrations.mount.MountCapability.UNMOUNT_FORCED; @Singleton public class Mounter { - private final Settings settings; + // mount providers (key) can not be used if any of the conflicting mount providers (values) are already in use + private static final Map> CONFLICTING_MOUNT_SERVICES = Map.of( + "org.cryptomator.frontend.fuse.mount.MacFuseMountProvider", Set.of("org.cryptomator.frontend.fuse.mount.FuseTMountProvider"), + "org.cryptomator.frontend.fuse.mount.FuseTMountProvider", Set.of("org.cryptomator.frontend.fuse.mount.MacFuseMountProvider") + ); + private final Environment env; + private final Settings settings; private final WindowsDriveLetters driveLetters; - private final ObservableValue mountServiceObservable; + private final List mountProviders; + private final Set usedMountServices; + private final ObservableValue defaultMountService; @Inject - public Mounter(Settings settings, Environment env, WindowsDriveLetters driveLetters, ObservableValue mountServiceObservable) { - this.settings = settings; + public Mounter(Environment env, // + Settings settings, // + WindowsDriveLetters driveLetters, // + List mountProviders, // + @Named("usedMountServices") Set usedMountServices, // + ObservableValue defaultMountService) { this.env = env; + this.settings = settings; this.driveLetters = driveLetters; - this.mountServiceObservable = mountServiceObservable; + this.mountProviders = mountProviders; + this.usedMountServices = usedMountServices; + this.defaultMountService = defaultMountService; } private class SettledMounter { - private MountService service; - private MountBuilder builder; - private VaultSettings vaultSettings; + private final MountService service; + private final MountBuilder builder; + private final VaultSettings vaultSettings; public SettledMounter(MountService service, MountBuilder builder, VaultSettings vaultSettings) { this.service = service; @@ -53,8 +72,13 @@ public class Mounter { for (var capability : service.capabilities()) { switch (capability) { case FILE_SYSTEM_NAME -> builder.setFileSystemName("cryptoFs"); - case LOOPBACK_PORT -> - builder.setLoopbackPort(settings.port.get()); //TODO: move port from settings to vaultsettings (see https://github.com/cryptomator/cryptomator/tree/feature/mount-setting-per-vault) + case LOOPBACK_PORT -> { + if (vaultSettings.mountService.getValue() == null) { + builder.setLoopbackPort(settings.port.get()); + } else { + builder.setLoopbackPort(vaultSettings.port.get()); + } + } case LOOPBACK_HOST_NAME -> env.getLoopbackAlias().ifPresent(builder::setLoopbackHostName); case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode.get()); case MOUNT_FLAGS -> { @@ -131,13 +155,26 @@ public class Mounter { } public MountHandle mount(VaultSettings vaultSettings, Path cryptoFsRoot) throws IOException, MountFailedException { - var mountService = this.mountServiceObservable.getValue().service(); + var mountService = mountProviders.stream().filter(s -> s.getClass().getName().equals(vaultSettings.mountService.getValue())).findFirst().orElse(defaultMountService.getValue()); + + if (isConflictingMountService(mountService)) { + var msg = STR."\{mountService.getClass()} unavailable due to conflict with either of \{CONFLICTING_MOUNT_SERVICES.get(mountService.getClass().getName())}"; + throw new ConflictingMountServiceException(msg); + } + + usedMountServices.add(mountService); + var builder = mountService.forFileSystem(cryptoFsRoot); - var internal = new SettledMounter(mountService, builder, vaultSettings); + var internal = new SettledMounter(mountService, builder, vaultSettings); // FIXME: no need for an inner class var cleanup = internal.prepare(); return new MountHandle(builder.mount(), mountService.hasCapability(UNMOUNT_FORCED), cleanup); } + public boolean isConflictingMountService(MountService service) { + var conflictingServices = CONFLICTING_MOUNT_SERVICES.getOrDefault(service.getClass().getName(), Set.of()); + return usedMountServices.stream().map(MountService::getClass).map(Class::getName).anyMatch(conflictingServices::contains); + } + public record MountHandle(Mount mountObj, boolean supportsUnmountForced, Runnable specialCleanup) { } diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 6662f61ff..fd21fc197 100644 --- a/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -8,7 +8,6 @@ package org.cryptomator.common.settings; import com.google.common.base.CharMatcher; import com.google.common.base.Strings; import com.google.common.io.BaseEncoding; -import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.VisibleForTesting; import javafx.beans.Observable; @@ -40,6 +39,7 @@ public class VaultSettings { static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK; static final boolean DEFAULT_AUTOLOCK_WHEN_IDLE = false; static final int DEFAULT_AUTOLOCK_IDLE_SECONDS = 30 * 60; + static final int DEFAULT_PORT = 42427; private static final Random RNG = new Random(); @@ -56,6 +56,8 @@ public class VaultSettings { public final IntegerProperty autoLockIdleSeconds; public final ObjectProperty mountPoint; public final StringExpression mountName; + public final StringProperty mountService; + public final IntegerProperty port; VaultSettings(VaultSettingsJson json) { this.id = json.id; @@ -70,6 +72,8 @@ public class VaultSettings { this.autoLockWhenIdle = new SimpleBooleanProperty(this, "autoLockWhenIdle", json.autoLockWhenIdle); this.autoLockIdleSeconds = new SimpleIntegerProperty(this, "autoLockIdleSeconds", json.autoLockIdleSeconds); this.mountPoint = new SimpleObjectProperty<>(this, "mountPoint", json.mountPoint == null ? null : Path.of(json.mountPoint)); + this.mountService = new SimpleStringProperty(this, "mountService", json.mountService); + this.port = new SimpleIntegerProperty(this, "port", json.port); // mount name is no longer an explicit setting, see https://github.com/cryptomator/cryptomator/pull/1318 this.mountName = StringExpression.stringExpression(Bindings.createStringBinding(() -> { final String name; @@ -95,7 +99,7 @@ public class VaultSettings { } Observable[] observables() { - return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode}; + return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode, port, mountService}; } public static VaultSettings withRandomId() { @@ -124,6 +128,8 @@ public class VaultSettings { json.autoLockWhenIdle = autoLockWhenIdle.get(); json.autoLockIdleSeconds = autoLockIdleSeconds.get(); json.mountPoint = mountPoint.map(Path::toString).getValue(); + json.mountService = mountService.get(); + json.port = port.get(); return json; } diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java b/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java index 2381203e5..43aa204e8 100644 --- a/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java +++ b/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java @@ -45,6 +45,12 @@ class VaultSettingsJson { @JsonProperty("autoLockIdleSeconds") int autoLockIdleSeconds = VaultSettings.DEFAULT_AUTOLOCK_IDLE_SECONDS; + @JsonProperty("mountService") + String mountService; + + @JsonProperty("port") + int port = VaultSettings.DEFAULT_PORT; + @Deprecated(since = "1.7.0") @JsonProperty(value = "winDriveLetter", access = JsonProperty.Access.WRITE_ONLY) // WRITE_ONLY means value is "written" into the java object during deserialization. Upvote this: https://github.com/FasterXML/jackson-annotations/issues/233 String winDriveLetter; diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 2e1e34a78..ac913d316 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -11,7 +11,6 @@ package org.cryptomator.common.vaults; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Constants; import org.cryptomator.common.mount.Mounter; -import org.cryptomator.common.mount.WindowsDriveLetters; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.cryptofs.CryptoFileSystem; import org.cryptomator.cryptofs.CryptoFileSystemProperties; @@ -73,7 +72,13 @@ public class Vault { private final AtomicReference mountHandle = new AtomicReference<>(null); @Inject - Vault(VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty lastKnownException, VaultStats stats, WindowsDriveLetters windowsDriveLetters, Mounter mounter) { + Vault(VaultSettings vaultSettings, // + VaultConfigCache configCache, // + AtomicReference cryptoFileSystem, // + VaultState state, // + @Named("lastKnownException") ObjectProperty lastKnownException, // + VaultStats stats, // + Mounter mounter) { this.vaultSettings = vaultSettings; this.configCache = configCache; this.cryptoFileSystem = cryptoFileSystem; diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index d5308b5c0..bd3233a7f 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -45,6 +45,7 @@ public enum FxmlFile { REMOVE_VAULT("/fxml/remove_vault.fxml"), // UPDATE_REMINDER("/fxml/update_reminder.fxml"), // UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"), + UNLOCK_REQUIRES_RESTART("/fxml/unlock_requires_restart.fxml"), // UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), // UNLOCK_SELECT_MASTERKEYFILE("/fxml/unlock_select_masterkeyfile.fxml"), // UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java index 653c4c6e6..8cb49a679 100644 --- a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java @@ -2,17 +2,14 @@ package org.cryptomator.ui.preferences; import dagger.Lazy; import org.cryptomator.common.ObservableUtil; -import org.cryptomator.common.mount.MountModule; import org.cryptomator.common.settings.Settings; import org.cryptomator.integrations.mount.MountCapability; import org.cryptomator.integrations.mount.MountService; import org.cryptomator.ui.common.FxController; import javax.inject.Inject; -import javax.inject.Named; import javafx.application.Application; import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanExpression; import javafx.beans.value.ObservableValue; import javafx.scene.control.Button; import javafx.scene.control.ChoiceBox; @@ -21,24 +18,22 @@ import javafx.util.StringConverter; import java.util.List; import java.util.Optional; import java.util.ResourceBundle; -import java.util.concurrent.atomic.AtomicReference; @PreferencesScoped public class VolumePreferencesController implements FxController { - private static final String DOCS_MOUNTING_URL = "https://docs.cryptomator.org/en/1.7/desktop/volume-type/"; - private static final int MIN_PORT = 1024; - private static final int MAX_PORT = 65535; + public static final String DOCS_MOUNTING_URL = "https://docs.cryptomator.org/en/1.7/desktop/volume-type/"; + public static final int MIN_PORT = 1024; + public static final int MAX_PORT = 65535; private final Settings settings; private final ObservableValue selectedMountService; private final ResourceBundle resourceBundle; - private final BooleanExpression loopbackPortSupported; + private final ObservableValue loopbackPortSupported; private final ObservableValue mountToDirSupported; private final ObservableValue mountToDriveLetterSupported; private final ObservableValue mountFlagsSupported; private final ObservableValue readonlySupported; - private final ObservableValue fuseRestartRequired; private final Lazy application; private final List mountProviders; public ChoiceBox volumeTypeChoiceBox; @@ -46,7 +41,10 @@ public class VolumePreferencesController implements FxController { public Button loopbackPortApplyButton; @Inject - VolumePreferencesController(Settings settings, Lazy application, List mountProviders, @Named("FUPFMS") AtomicReference firstUsedProblematicFuseMountService, ResourceBundle resourceBundle) { + VolumePreferencesController(Settings settings, // + Lazy application, // + List mountProviders, // + ResourceBundle resourceBundle) { this.settings = settings; this.application = application; this.mountProviders = mountProviders; @@ -54,17 +52,11 @@ public class VolumePreferencesController implements FxController { var fallbackProvider = mountProviders.stream().findFirst().orElse(null); this.selectedMountService = ObservableUtil.mapWithDefault(settings.mountService, serviceName -> mountProviders.stream().filter(s -> s.getClass().getName().equals(serviceName)).findFirst().orElse(fallbackProvider), fallbackProvider); - this.loopbackPortSupported = BooleanExpression.booleanExpression(selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT))); + this.loopbackPortSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT)); this.mountToDirSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT) || s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR)); this.mountToDriveLetterSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER)); this.mountFlagsSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS)); this.readonlySupported = selectedMountService.map(s -> s.hasCapability(MountCapability.READ_ONLY)); - this.fuseRestartRequired = selectedMountService.map(s -> {// - return firstUsedProblematicFuseMountService.get() != null // - && MountModule.isProblematicFuseService(s) // - && !firstUsedProblematicFuseMountService.get().equals(s); - }); - } public void initialize() { @@ -101,12 +93,12 @@ public class VolumePreferencesController implements FxController { /* Property Getters */ - public BooleanExpression loopbackPortSupportedProperty() { + public ObservableValue loopbackPortSupportedProperty() { return loopbackPortSupported; } public boolean isLoopbackPortSupported() { - return loopbackPortSupported.get(); + return loopbackPortSupported.getValue(); } public ObservableValue readonlySupportedProperty() { @@ -141,14 +133,6 @@ public class VolumePreferencesController implements FxController { return mountFlagsSupported.getValue(); } - public ObservableValue fuseRestartRequiredProperty() { - return fuseRestartRequired; - } - - public boolean getFuseRestartRequired() { - return fuseRestartRequired.getValue(); - } - /* Helpers */ private class MountServiceConverter extends StringConverter { diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java b/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java index 67e905200..825d0fc2d 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java @@ -19,16 +19,8 @@ import java.util.concurrent.Future; @Subcomponent(modules = {UnlockModule.class}) public interface UnlockComponent { - ExecutorService defaultExecutorService(); - UnlockWorkflow unlockWorkflow(); - default Future startUnlockWorkflow() { - UnlockWorkflow workflow = unlockWorkflow(); - defaultExecutorService().submit(workflow); - return workflow; - } - @Subcomponent.Factory interface Factory { UnlockComponent create(@BindsInstance @UnlockWindow Vault vault, @BindsInstance @Named("unlockWindowOwner") @Nullable Stage owner); diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index f93999d21..95f13d383 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java @@ -81,6 +81,13 @@ abstract class UnlockModule { return fxmlLoaders.createScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT); } + @Provides + @FxmlScene(FxmlFile.UNLOCK_REQUIRES_RESTART) + @UnlockScoped + static Scene provideRestartRequiredScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.UNLOCK_REQUIRES_RESTART); + } + // ------------------ @Binds @@ -93,4 +100,9 @@ abstract class UnlockModule { @FxControllerKey(UnlockInvalidMountPointController.class) abstract FxController bindUnlockInvalidMountPointController(UnlockInvalidMountPointController controller); + @Binds + @IntoMap + @FxControllerKey(UnlockRequiresRestartController.class) + abstract FxController bindUnlockRequiresRestartController(UnlockRequiresRestartController controller); + } diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockRequiresRestartController.java b/src/main/java/org/cryptomator/ui/unlock/UnlockRequiresRestartController.java new file mode 100644 index 000000000..497194dff --- /dev/null +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockRequiresRestartController.java @@ -0,0 +1,47 @@ +package org.cryptomator.ui.unlock; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.fxapp.FxApplicationWindows; +import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab; + +import javax.inject.Inject; +import javafx.fxml.FXML; +import javafx.stage.Stage; +import java.util.ResourceBundle; + +@UnlockScoped +public class UnlockRequiresRestartController implements FxController { + + private final Stage window; + private final ResourceBundle resourceBundle; + private final FxApplicationWindows appWindows; + private final Vault vault; + + @Inject + UnlockRequiresRestartController(@UnlockWindow Stage window, // + ResourceBundle resourceBundle, // + FxApplicationWindows appWindows, // + @UnlockWindow Vault vault) { + this.window = window; + this.resourceBundle = resourceBundle; + this.appWindows = appWindows; + this.vault = vault; + } + + public void initialize() { + window.setTitle(String.format(resourceBundle.getString("unlock.error.title"), vault.getDisplayName())); + } + + @FXML + public void close() { + window.close(); + } + + @FXML + public void closeAndOpenVaultOptions() { + appWindows.showVaultOptionsWindow(vault, SelectedVaultOptionsTab.MOUNT); + window.close(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index 564d57ab6..98a49dec5 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -1,7 +1,7 @@ package org.cryptomator.ui.unlock; -import com.google.common.base.Throwables; import dagger.Lazy; +import org.cryptomator.common.mount.ConflictingMountServiceException; import org.cryptomator.common.mount.IllegalMountPointException; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultState; @@ -29,7 +29,7 @@ import java.io.IOException; * This class runs the unlock process and controls when to display which UI. */ @UnlockScoped -public class UnlockWorkflow extends Task { +public class UnlockWorkflow extends Task { private static final Logger LOG = LoggerFactory.getLogger(UnlockWorkflow.class); @@ -38,42 +38,44 @@ public class UnlockWorkflow extends Task { private final VaultService vaultService; private final Lazy successScene; private final Lazy invalidMountPointScene; + private final Lazy restartRequiredScene; private final FxApplicationWindows appWindows; private final KeyLoadingStrategy keyLoadingStrategy; private final ObjectProperty illegalMountPointException; @Inject - UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, @UnlockWindow ObjectProperty illegalMountPointException) { + UnlockWorkflow(@UnlockWindow Stage window, // + @UnlockWindow Vault vault, // + VaultService vaultService, // + @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, // + @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, // + @FxmlScene(FxmlFile.UNLOCK_REQUIRES_RESTART) Lazy restartRequiredScene, // + FxApplicationWindows appWindows, // + @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, // + @UnlockWindow ObjectProperty illegalMountPointException) { this.window = window; this.vault = vault; this.vaultService = vaultService; this.successScene = successScene; this.invalidMountPointScene = invalidMountPointScene; + this.restartRequiredScene = restartRequiredScene; this.appWindows = appWindows; this.keyLoadingStrategy = keyLoadingStrategy; this.illegalMountPointException = illegalMountPointException; } @Override - protected Boolean call() throws InterruptedException, IOException, CryptoException, MountFailedException { - try { - attemptUnlock(); - return true; - } catch (UnlockCancelledException e) { - cancel(false); // set Tasks state to cancelled - return false; - } - } - - private void attemptUnlock() throws IOException, CryptoException, MountFailedException { + protected Void call() throws InterruptedException, IOException, CryptoException, MountFailedException { try { keyLoadingStrategy.use(vault::unlock); + return null; + } catch (UnlockCancelledException e) { + cancel(false); // set Tasks state to cancelled + return null; + } catch (IOException | RuntimeException | MountFailedException e) { + throw e; } catch (Exception e) { - Throwables.propagateIfPossible(e, IOException.class); - Throwables.propagateIfPossible(e, CryptoException.class); - Throwables.propagateIfPossible(e, IllegalMountPointException.class); - Throwables.propagateIfPossible(e, MountFailedException.class); - throw new IllegalStateException("unexpected exception type", e); + throw new IllegalStateException("Unexpected exception type", e); } } @@ -85,6 +87,13 @@ public class UnlockWorkflow extends Task { }); } + private void handleConflictingMountServiceException() { + Platform.runLater(() -> { + window.setScene(restartRequiredScene.get()); + window.show(); + }); + } + private void handleGenericError(Throwable e) { LOG.error("Unlock failed for technical reasons.", e); appWindows.showErrorWindow(e, window, null); @@ -113,10 +122,10 @@ public class UnlockWorkflow extends Task { protected void failed() { LOG.info("Unlock of '{}' failed.", vault.getDisplayName()); Throwable throwable = super.getException(); - if(throwable instanceof IllegalMountPointException impe) { - handleIllegalMountPointError(impe); - } else { - handleGenericError(throwable); + switch (throwable) { + case IllegalMountPointException e -> handleIllegalMountPointError(e); + case ConflictingMountServiceException _ -> handleConflictingMountServiceException(); + default -> handleGenericError(throwable); } vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED); } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java index 5eeab43e0..106623985 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java @@ -1,18 +1,25 @@ package org.cryptomator.ui.vaultoptions; import com.google.common.base.Strings; -import org.cryptomator.common.mount.ActualMountService; +import dagger.Lazy; +import org.cryptomator.common.ObservableUtil; +import org.cryptomator.common.mount.Mounter; import org.cryptomator.common.mount.WindowsDriveLetters; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.vaults.Vault; import org.cryptomator.integrations.mount.MountCapability; +import org.cryptomator.integrations.mount.MountService; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.cryptomator.ui.preferences.SelectedPreferencesTab; +import org.cryptomator.ui.preferences.VolumePreferencesController; import javax.inject.Inject; +import javafx.application.Application; +import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; +import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; import javafx.scene.control.RadioButton; @@ -26,6 +33,8 @@ import java.io.File; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.List; +import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; @@ -36,14 +45,21 @@ public class MountOptionsController implements FxController { private final VaultSettings vaultSettings; private final WindowsDriveLetters windowsDriveLetters; private final ResourceBundle resourceBundle; + private final Lazy application; private final ObservableValue defaultMountFlags; private final ObservableValue mountpointDirSupported; private final ObservableValue mountpointDriveLetterSupported; private final ObservableValue readOnlySupported; private final ObservableValue mountFlagsSupported; + private final ObservableValue defaultMountServiceSelected; private final ObservableValue directoryPath; private final FxApplicationWindows applicationWindows; + private final List mountProviders; + private final ObservableValue defaultMountService; + private final ObservableValue selectedMountService; + private final ObservableValue selectedMountServiceRequiresRestart; + private final ObservableValue loopbackPortChangeable; //-- FXML objects -- @@ -56,30 +72,58 @@ public class MountOptionsController implements FxController { public RadioButton mountPointDirBtn; public TextField directoryPathField; public ChoiceBox driveLetterSelection; + public ChoiceBox vaultVolumeTypeChoiceBox; + public TextField vaultLoopbackPortField; + public Button vaultLoopbackPortApplyButton; + @Inject - MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue mountService, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, FxApplicationWindows applicationWindows) { + MountOptionsController(@VaultOptionsWindow Stage window, // + @VaultOptionsWindow Vault vault, // + WindowsDriveLetters windowsDriveLetters, // + ResourceBundle resourceBundle, // + FxApplicationWindows applicationWindows, // + Lazy application, // + List mountProviders, // + Mounter mounter, // + ObservableValue defaultMountService) { this.window = window; this.vaultSettings = vault.getVaultSettings(); this.windowsDriveLetters = windowsDriveLetters; this.resourceBundle = resourceBundle; - this.defaultMountFlags = mountService.map(as -> { - if (as.service().hasCapability(MountCapability.MOUNT_FLAGS)) { - return as.service().getDefaultMountFlags(); + this.applicationWindows = applicationWindows; + this.directoryPath = vault.getVaultSettings().mountPoint.map(p -> isDriveLetter(p) ? null : p.toString()); + this.application = application; + this.mountProviders = mountProviders; + this.defaultMountService = defaultMountService; + this.selectedMountService = Bindings.createObjectBinding(this::reselectMountService, defaultMountService, vaultSettings.mountService); + this.selectedMountServiceRequiresRestart = selectedMountService.map(mounter::isConflictingMountService); + + this.defaultMountFlags = selectedMountService.map(s -> { + if (s.hasCapability(MountCapability.MOUNT_FLAGS)) { + return s.getDefaultMountFlags(); } else { return ""; } }); - this.mountpointDirSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR) || as.service().hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT)); - this.mountpointDriveLetterSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER)); - this.mountFlagsSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_FLAGS)); - this.readOnlySupported = mountService.map(as -> as.service().hasCapability(MountCapability.READ_ONLY)); - this.directoryPath = vault.getVaultSettings().mountPoint.map(p -> isDriveLetter(p) ? null : p.toString()); - this.applicationWindows = applicationWindows; + this.mountFlagsSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS)); + this.defaultMountServiceSelected = ObservableUtil.mapWithDefault(vaultSettings.mountService, _ -> false, true); + this.readOnlySupported = selectedMountService.map(s -> s.hasCapability(MountCapability.READ_ONLY)); + this.mountpointDirSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR) || s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT)); + this.mountpointDriveLetterSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER)); + this.loopbackPortChangeable = selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT) && vaultSettings.mountService.getValue() != null); + } + + private MountService reselectMountService() { + var desired = vaultSettings.mountService.getValue(); + var defaultMS = defaultMountService.getValue(); + return mountProviders.stream().filter(s -> s.getClass().getName().equals(desired)).findFirst().orElse(defaultMS); } @FXML public void initialize() { + defaultMountService.addListener((_, _, _) -> vaultVolumeTypeChoiceBox.setConverter(new MountServiceConverter())); + // readonly: readOnlyCheckbox.selectedProperty().bindBidirectional(vaultSettings.usesReadOnlyMode); @@ -106,6 +150,20 @@ public class MountOptionsController implements FxController { mountPointToggleGroup.selectToggle(mountPointDirBtn); } mountPointToggleGroup.selectedToggleProperty().addListener(this::selectedToggleChanged); + + vaultVolumeTypeChoiceBox.getItems().add(null); + vaultVolumeTypeChoiceBox.getItems().addAll(mountProviders); + vaultVolumeTypeChoiceBox.setConverter(new MountServiceConverter()); + vaultVolumeTypeChoiceBox.getSelectionModel().select(isDefaultMountServiceSelected() ? null : selectedMountService.getValue()); + vaultVolumeTypeChoiceBox.valueProperty().addListener((_, _, newProvider) -> { + var toSet = Optional.ofNullable(newProvider).map(nP -> nP.getClass().getName()).orElse(null); + vaultSettings.mountService.set(toSet); + }); + + vaultLoopbackPortField.setText(String.valueOf(vaultSettings.port.get())); + vaultLoopbackPortApplyButton.visibleProperty().bind(vaultSettings.port.asString().isNotEqualTo(vaultLoopbackPortField.textProperty())); + vaultLoopbackPortApplyButton.disableProperty().bind(Bindings.createBooleanBinding(this::validateLoopbackPort, vaultLoopbackPortField.textProperty()).not()); + } @FXML @@ -229,6 +287,26 @@ public class MountOptionsController implements FxController { } + public void openDocs() { + application.get().getHostServices().showDocument(VolumePreferencesController.DOCS_MOUNTING_URL); + } + + private boolean validateLoopbackPort() { + try { + int port = Integer.parseInt(vaultLoopbackPortField.getText()); + return port == 0 // choose port automatically + || port >= VolumePreferencesController.MIN_PORT && port <= VolumePreferencesController.MAX_PORT; // port within range + } catch (NumberFormatException e) { + return false; + } + } + + public void doChangeLoopbackPort() { + if (validateLoopbackPort()) { + vaultSettings.port.set(Integer.parseInt(vaultLoopbackPortField.getText())); + } + } + //@formatter:off private static class NoDirSelectedException extends Exception {} //@formatter:on @@ -243,6 +321,14 @@ public class MountOptionsController implements FxController { return mountFlagsSupported.getValue(); } + public ObservableValue defaultMountServiceSelectedProperty() { + return defaultMountServiceSelected; + } + + public boolean isDefaultMountServiceSelected() { + return defaultMountServiceSelected.getValue(); + } + public ObservableValue mountpointDirSupportedProperty() { return mountpointDirSupported; } @@ -274,4 +360,37 @@ public class MountOptionsController implements FxController { public String getDirectoryPath() { return directoryPath.getValue(); } + + public ObservableValue selectedMountServiceRequiresRestartProperty() { + return selectedMountServiceRequiresRestart; + } + + public boolean getSelectedMountServiceRequiresRestart() { + return selectedMountServiceRequiresRestart.getValue(); + } + + public ObservableValue loopbackPortChangeableProperty() { + return loopbackPortChangeable; + } + + public boolean isLoopbackPortChangeable() { + return loopbackPortChangeable.getValue(); + } + + private class MountServiceConverter extends StringConverter { + + @Override + public String toString(MountService provider) { + if (provider == null) { + return String.format(resourceBundle.getString("vaultOptions.mount.volumeType.default"), defaultMountService.getValue().displayName()); + } else { + return provider.displayName(); + } + } + + @Override + public MountService fromString(String string) { + throw new UnsupportedOperationException(); + } + } } diff --git a/src/main/resources/fxml/preferences_volume.fxml b/src/main/resources/fxml/preferences_volume.fxml index f48b1c1c8..c173f03e2 100644 --- a/src/main/resources/fxml/preferences_volume.fxml +++ b/src/main/resources/fxml/preferences_volume.fxml @@ -32,8 +32,6 @@ -