diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 71e2fe336..8698d58ab 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -16,7 +16,7 @@ jobs: get-version: uses: ./.github/workflows/get-version.yml with: - version: ${{ github.event.inputs.version }} + version: ${{ inputs.version }} build: name: Build AppImage @@ -55,7 +55,7 @@ jobs: --verbose --output runtime --module-path "${JAVA_HOME}/jmods" - --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr + --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr --strip-native-commands --no-header-files --no-man-pages @@ -78,7 +78,7 @@ jobs: --dest appdir --name Cryptomator --vendor "Skymatic GmbH" - --copyright "(C) 2016 - 2022 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" diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 178d6ce96..c4d67319d 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -151,7 +151,7 @@ jobs: uses: softprops/action-gh-release@v1 with: fail_on_unmatched_files: true - tag_name: ${{ github.env.TAG_NAME }} + tag_name: ${{ inputs.ref }} token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} files: | cryptomator_*_amd64.deb diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index 7e9a8c0d5..55b1f325d 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -16,7 +16,7 @@ jobs: get-version: uses: ./.github/workflows/get-version.yml with: - version: ${{ github.event.inputs.version }} + version: ${{ inputs.version }} build: name: Build Cryptomator.app for ${{ matrix.output-suffix }} @@ -87,7 +87,7 @@ jobs: --dest appdir --name Cryptomator --vendor "Skymatic GmbH" - --copyright "(C) 2016 - 2022 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" diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index a20f58d44..0a75c44ca 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -22,7 +22,7 @@ jobs: get-version: uses: ./.github/workflows/get-version.yml with: - version: ${{ github.event.inputs.version }} + version: ${{ inputs.version }} build-msi: name: Build .msi Installer @@ -81,7 +81,7 @@ jobs: --dest appdir --name Cryptomator --vendor "Skymatic GmbH" - --copyright "(C) 2016 - 2022 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" @@ -118,6 +118,9 @@ jobs: - name: Fix permissions run: attrib -r appdir/Cryptomator/Cryptomator.exe shell: pwsh + - name: Extract integrations DLL for code signing + shell: pwsh + run: gci ./appdir/Cryptomator/app/mods/ -File integrations-win-*.jar | ForEach-Object {Set-Location -Path $_.Directory; jar --file=$($_.FullName) --extract integrations.dll } - name: Codesign uses: skymatic/code-sign-action@v2 with: @@ -128,6 +131,10 @@ jobs: timestampUrl: 'http://timestamp.digicert.com' folder: appdir/Cryptomator recursive: true + - name: Repack signed DLL into jar + shell: pwsh + run: | + gci ./appdir/Cryptomator/app/mods/ -File integrations-win-*.jar | ForEach-Object {Set-Location -Path $_.Directory; jar --file=$($_.FullName) --update integrations.dll; Remove-Item integrations.dll} - name: Generate license for MSI run: > mvn -B license:add-third-party @@ -149,7 +156,7 @@ jobs: --dest installer --name Cryptomator --vendor "Skymatic GmbH" - --copyright "(C) 2016 - 2022 Skymatic GmbH" + --copyright "(C) 2016 - 2023 Skymatic GmbH" --app-version "${{ needs.get-version.outputs.semVerNum }}" --win-menu --win-dir-chooser @@ -247,7 +254,7 @@ jobs: -out dist/win/bundle/ -dBundleVersion="${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}" -dBundleVendor="Skymatic GmbH" - -dBundleCopyright="(C) 2016 - 2022 Skymatic GmbH" + -dBundleCopyright="(C) 2016 - 2023 Skymatic GmbH" -dAboutUrl="https://cryptomator.org" -dHelpUrl="https://cryptomator.org/contact" -dUpdateUrl="https://cryptomator.org/downloads/" diff --git a/README.md b/README.md index fc7d7df55..b742846c2 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Cryptomator is provided free of charge as an open-source project despite the hig Mow Capital EaseUS + Hassmann IT-Forensik diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh index 06e3a7341..7fc9f04a8 100755 --- a/dist/linux/appimage/build.sh +++ b/dist/linux/appimage/build.sh @@ -11,15 +11,20 @@ command -v curl >/dev/null 2>&1 || { echo >&2 "curl not found."; exit 1; } VERSION=$(mvn -f ../../../pom.xml help:evaluate -Dexpression=project.version -q -DforceStdout) SEMVER_STR=${VERSION} +mvn -f ../../../pom.xml versions:set -DnewVersion=${SEMVER_STR} + # compile -mvn -B -f ../../../pom.xml clean package -DskipTests -Plinux +mvn -B -f ../../../pom.xml clean package -Plinux -DskipTests +cp ../../../LICENSE.txt ../../../target +cp ../launcher.sh ../../../target cp ../../../target/cryptomator-*.jar ../../../target/mods # add runtime ${JAVA_HOME}/bin/jlink \ + --verbose \ --output runtime \ --module-path "${JAVA_HOME}/jmods" \ - --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \ + --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr \ --strip-native-commands \ --no-header-files \ --no-man-pages \ @@ -27,7 +32,7 @@ ${JAVA_HOME}/bin/jlink \ --compress=1 # create app dir -envsubst '${SEMVER_STR} ${REVISION_NUM}' < dist/linux/launcher-gtk2.properties > launcher-gtk2.properties +envsubst '${SEMVER_STR} ${REVISION_NUM}' < ../launcher-gtk2.properties > launcher-gtk2.properties ${JAVA_HOME}/bin/jpackage \ --verbose \ --type app-image \ @@ -35,12 +40,10 @@ ${JAVA_HOME}/bin/jpackage \ --input ../../../target/libs \ --module-path ../../../target/mods \ --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator \ - --dest . \ + --dest appdir \ --name Cryptomator \ --vendor "Skymatic GmbH" \ - --copyright "(C) 2016 - 2022 Skymatic GmbH" \ - --java-options "--enable-preview" \ - --java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" \ + --copyright "(C) 2016 - 2023 Skymatic GmbH" \ --java-options "-Xss5m" \ --java-options "-Xmx256m" \ --app-version "${VERSION}.${REVISION_NO}" \ @@ -48,6 +51,7 @@ ${JAVA_HOME}/bin/jpackage \ --java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\"" \ --java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\"" \ --java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" \ + --java-options "-Dcryptomator.p12Path=\"~/.config/Cryptomator/key.p12\"" \ --java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" \ --java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" \ --java-options "-Dcryptomator.showTrayIcon=false" \ @@ -56,9 +60,8 @@ ${JAVA_HOME}/bin/jpackage \ --resource-dir ../resources # transform AppDir -mv Cryptomator Cryptomator.AppDir +mv appdir/Cryptomator Cryptomator.AppDir cp -r resources/AppDir/* Cryptomator.AppDir/ -chmod +x Cryptomator.AppDir/lib/runtime/bin/java envsubst '${REVISION_NO}' < resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh cp ../common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png cp ../common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png @@ -85,5 +88,11 @@ chmod +x /tmp/appimagetool.AppImage # create AppImage /tmp/appimagetool.AppImage \ Cryptomator.AppDir \ - cryptomator-SNAPSHOT-x86_64.AppImage \ + cryptomator-${SEMVER_STR}-x86_64.AppImage \ -u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync' + +echo "" +echo "Done. AppImage successfully created: cryptomator-${SEMVER_STR}-x86_64.AppImage" +echo "" +echo >&2 "To clean up, run: rm -rf Cryptomator.AppDir appdir jni runtime squashfs-root; rm launcher-gtk2.properties /tmp/appimagetool.AppImage" +echo "" \ No newline at end of file diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml index 9cb44d090..9797dd6a7 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/dist/linux/debian/copyright b/dist/linux/debian/copyright index 34be0a4c9..745218b67 100644 --- a/dist/linux/debian/copyright +++ b/dist/linux/debian/copyright @@ -4,11 +4,11 @@ Upstream-Contact: Cryptomator Source: https://cryptomator.org Files: * -Copyright: 2016-2022 Skymatic GmbH +Copyright: 2016-2023 Skymatic GmbH License: GPL-3+ Files: debian/org.cryptomator.Cryptomator.appdata.xml -Copyright: 2016-2022 Skymatic GmbH +Copyright: 2016-2023 Skymatic GmbH License: FSFAP License: GPL-3+ diff --git a/dist/linux/debian/rules b/dist/linux/debian/rules index e35dbb0a7..ec82dea47 100755 --- a/dist/linux/debian/rules +++ b/dist/linux/debian/rules @@ -27,7 +27,7 @@ override_dh_auto_build: $(JAVA_HOME)/bin/jlink \ --output runtime \ --module-path "${JMODS_PATH}" \ - --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \ + --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr \ --strip-native-commands \ --no-header-files \ --no-man-pages \ @@ -42,9 +42,7 @@ override_dh_auto_build: --dest . \ --name cryptomator \ --vendor "Skymatic GmbH" \ - --copyright "(C) 2016 - 2022 Skymatic GmbH" \ - --java-options "--enable-preview" \ - --java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" \ + --copyright "(C) 2016 - 2023 Skymatic GmbH" \ --java-options "-Xss5m" \ --java-options "-Xmx256m" \ --java-options "-Dfile.encoding=\"utf-8\"" \ diff --git a/dist/mac/dmg/build.sh b/dist/mac/dmg/build.sh index 906072ebd..75044bf86 100755 --- a/dist/mac/dmg/build.sh +++ b/dist/mac/dmg/build.sh @@ -21,7 +21,7 @@ rm -rf runtime dmg *.app *.dmg # set variables APP_NAME="Cryptomator" VENDOR="Skymatic GmbH" -COPYRIGHT_YEARS="2016 - 2022" +COPYRIGHT_YEARS="2016 - 2023" PACKAGE_IDENTIFIER="org.cryptomator" MAIN_JAR_GLOB="cryptomator-*.jar" MODULE_AND_MAIN_CLASS="org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator" diff --git a/dist/mac/dmg/resources/licenseTemplate.ftl b/dist/mac/dmg/resources/licenseTemplate.ftl index e4d7fd476..98178151c 100644 --- a/dist/mac/dmg/resources/licenseTemplate.ftl +++ b/dist/mac/dmg/resources/licenseTemplate.ftl @@ -17,7 +17,7 @@ \f1\b0 \ \ -\f0\b \'a9 2016 \'96 2022 Skymatic GmbH +\f0\b \'a9 2016 \'96 2023 Skymatic GmbH \f1\b0 \ \ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\ diff --git a/dist/mac/resources/Cryptomator.icns b/dist/mac/resources/Cryptomator.icns index 25da5b5be..dfdb4499d 100644 Binary files a/dist/mac/resources/Cryptomator.icns and b/dist/mac/resources/Cryptomator.icns differ diff --git a/dist/win/bundle/resources/licenseTemplate.ftl b/dist/win/bundle/resources/licenseTemplate.ftl index 8a568b85d..40d55e292 100644 --- a/dist/win/bundle/resources/licenseTemplate.ftl +++ b/dist/win/bundle/resources/licenseTemplate.ftl @@ -10,7 +10,7 @@ \vieww12000\viewh15840\viewkind0 \pard\tx283\tx567\tx850\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\b\fs16\lang7 Cryptomator is distributed under the GPLv3 License, found below. Please see the bottom of this document for any other license applicable to code used within Cryptomator.\b0\par \par -\b\'a9 2016 \'96 2022 Skymatic GmbH \b0\par +\b\'a9 2016 \'96 2023 Skymatic GmbH \b0\par \par This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\par \par diff --git a/dist/win/resources/licenseTemplate.ftl b/dist/win/resources/licenseTemplate.ftl index 0ee793cb1..88a80f8b6 100644 --- a/dist/win/resources/licenseTemplate.ftl +++ b/dist/win/resources/licenseTemplate.ftl @@ -10,7 +10,7 @@ \vieww12000\viewh15840\viewkind0 \pard\tx283\tx567\tx850\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\b\fs16\lang7 Cryptomator is distributed under the GPLv3 License, found below. Please see the bottom of this document for any other license applicable to code used within Cryptomator.\b0\par \par -\b\'a9 2016 \'96 2022 Skymatic GmbH \b0\par +\b\'a9 2016 \'96 2023 Skymatic GmbH \b0\par \par This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\par \par diff --git a/pom.xml b/pom.xml index b2fde6904..171a97083 100644 --- a/pom.xml +++ b/pom.xml @@ -24,18 +24,19 @@ 19 - - com.github.jnr,org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh + + org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava - 2.5.3 - 1.2.0-beta2 - 1.1.2 - 1.1.2 - 1.1.0 - 2.0.0-beta2 - 2.0.0-beta1 - 2.0.0-beta1 + 2.1.1 + 2.6.1 + 1.2.0-beta4 + 1.2.0-beta1 + 1.2.0-beta2 + 1.2.0-beta1 + 2.0.0-beta4 + 2.0.0-beta2 + 2.0.0-beta4 3.12.0 @@ -58,12 +59,18 @@ 23.0.0 - 7.4.0 + 7.4.4 0.8.8 + + + org.cryptomator + cryptolib + ${cryptomator.cryptolib.version} + org.cryptomator cryptofs 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 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 index ac2e5f598..d78cc3216 100644 --- a/src/main/java/org/cryptomator/common/mount/MountModule.java +++ b/src/main/java/org/cryptomator/common/mount/MountModule.java @@ -6,6 +6,7 @@ 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; @@ -18,13 +19,33 @@ public class MountModule { 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) { - return settings.mountService().map(desiredServiceImpl -> { - var fallbackProvider = serviceImpls.stream().findFirst().orElse(null); - return serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(desiredServiceImpl)).findAny().orElse(fallbackProvider); - }); + 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/settings/DeviceKey.java b/src/main/java/org/cryptomator/common/settings/DeviceKey.java index 04e9ebd0f..d3431440a 100644 --- a/src/main/java/org/cryptomator/common/settings/DeviceKey.java +++ b/src/main/java/org/cryptomator/common/settings/DeviceKey.java @@ -76,6 +76,11 @@ public class DeviceKey { private P384KeyPair createAndStoreNewKeyPair(char[] passphrase, Path p12File) throws IOException { var keyPair = P384KeyPair.generate(); + var tmpFile = p12File.resolveSibling(p12File.getFileName().toString() + ".tmp"); + if(Files.exists(tmpFile)) { + LOG.debug("Leftover from devicekey creation detected. Deleting {}", tmpFile); + Files.delete(tmpFile); + } LOG.debug("Store new device key to {}", p12File); keyPair.store(p12File, passphrase); return keyPair; diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java index 91299bb2a..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; @@ -73,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(); @@ -105,18 +108,49 @@ public class SettingsJsonAdapter extends TypeAdapter { 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 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"; + } + } + } + private UiTheme parseUiTheme(String uiThemeName) { try { return UiTheme.valueOf(uiThemeName.toUpperCase()); diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 57c114718..3a116b4ce 100644 --- a/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -45,7 +45,7 @@ public class VaultSettings { private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP); private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REVEAL_AFTER_MOUNT); 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); diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java index 0f1aa7edd..cffeb3aa7 100644 --- a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java +++ b/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java @@ -38,8 +38,6 @@ class VaultSettingsJsonAdapter { out.endObject(); } - //TODO: usesCustomMountPath, customMountPath and winDriveLetter removed - // -> migration required public VaultSettings read(JsonReader in) throws IOException { String id = null; String path = null; @@ -55,6 +53,12 @@ class VaultSettingsJsonAdapter { 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(); @@ -78,6 +82,11 @@ class VaultSettingsJsonAdapter { 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(); @@ -102,6 +111,13 @@ class VaultSettingsJsonAdapter { 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; } 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/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 835560120..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,10 @@ *******************************************************************************/ 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.Environment; +import org.cryptomator.common.mount.Mounter; import org.cryptomator.common.mount.WindowsDriveLetters; -import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.cryptofs.CryptoFileSystem; import org.cryptomator.cryptofs.CryptoFileSystemProperties; @@ -23,11 +21,7 @@ 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.Mount; -import org.cryptomator.integrations.mount.MountBuilder; -import org.cryptomator.integrations.mount.MountCapability; import org.cryptomator.integrations.mount.MountFailedException; -import org.cryptomator.integrations.mount.MountService; import org.cryptomator.integrations.mount.Mountpoint; import org.cryptomator.integrations.mount.UnmountFailedException; import org.slf4j.Logger; @@ -44,9 +38,7 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.value.ObservableValue; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.EnumSet; @@ -54,11 +46,6 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; -import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER; -import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR; -import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_SYSTEM_CHOSEN_PATH; -import static org.cryptomator.integrations.mount.MountCapability.MOUNT_WITHIN_EXISTING_PARENT; - @PerVault public class Vault { @@ -66,14 +53,10 @@ public class Vault { private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME); private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE; - private final Environment env; - private final Settings settings; private final VaultSettings vaultSettings; private final AtomicReference cryptoFileSystem; private final VaultState state; private final ObjectProperty lastKnownException; - private final ObservableValue mountService; - private final ObservableValue defaultMountFlags; private final VaultConfigCache configCache; private final VaultStats stats; private final StringBinding displayablePath; @@ -84,22 +67,18 @@ public class Vault { private final BooleanBinding needsMigration; private final BooleanBinding unknownError; private final ObjectBinding mountPoint; - private final WindowsDriveLetters windowsDriveLetters; + private final Mounter mounter; private final BooleanProperty showingStats; - private AtomicReference mountHandle = new AtomicReference<>(null); + private AtomicReference mountHandle = new AtomicReference<>(null); @Inject - Vault(Environment env, Settings settings, VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty lastKnownException, ObservableValue mountService, VaultStats stats, WindowsDriveLetters windowsDriveLetters) { - this.env = env; - this.settings = settings; + 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.cryptoFileSystem = cryptoFileSystem; this.state = state; this.lastKnownException = lastKnownException; - this.mountService = mountService; - this.defaultMountFlags = mountService.map(MountService::getDefaultMountFlags); this.stats = stats; this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path()); this.locked = Bindings.createBooleanBinding(this::isLocked, state); @@ -109,7 +88,7 @@ public class Vault { this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state); this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state); this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state); - this.windowsDriveLetters = windowsDriveLetters; + this.mounter = mounter; this.showingStats = new SimpleBooleanProperty(false); } @@ -159,45 +138,6 @@ public class Vault { } } - private MountBuilder prepareMount(Path cryptoRoot) throws IOException { - var mountProvider = mountService.getValue(); - var builder = mountProvider.forFileSystem(cryptoRoot); - - for (var capability : mountProvider.capabilities()) { - switch (capability) { - case FILE_SYSTEM_NAME -> builder.setFileSystemName("crypto"); - case LOOPBACK_PORT -> builder.setLoopbackPort(settings.port().get()); //TODO: move port from settings to vaultsettings? - case LOOPBACK_HOST_NAME -> builder.setLoopbackHostName("cryptomator-vault"); //TODO: Read from system property - case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode().get()); - case MOUNT_FLAGS -> builder.setMountFlags(defaultMountFlags.getValue()); // TODO use custom mount flags (pre-populated with default mount flags) - case VOLUME_ID -> builder.setVolumeId(vaultSettings.getId()); - case VOLUME_NAME -> builder.setVolumeName(vaultSettings.mountName().get()); - } - } - - var userChosenMountPoint = vaultSettings.getMountPoint(); - var defaultMountPointBase = env.getMountPointsDir().orElseThrow(); - if (userChosenMountPoint == null) { - if (mountProvider.hasCapability(MOUNT_TO_SYSTEM_CHOSEN_PATH)) { - // no need to set a mount point - } else if (mountProvider.hasCapability(MOUNT_AS_DRIVE_LETTER)) { - builder.setMountpoint(windowsDriveLetters.getFirstDesiredAvailable().orElseThrow()); - } else if (mountProvider.hasCapability(MOUNT_WITHIN_EXISTING_PARENT)) { - Files.createDirectories(defaultMountPointBase); - builder.setMountpoint(defaultMountPointBase); - } else if (mountProvider.hasCapability(MOUNT_TO_EXISTING_DIR) ) { - var mountPoint = defaultMountPointBase.resolve(vaultSettings.mountName().get()); - Files.createDirectories(mountPoint); - builder.setMountpoint(mountPoint); - } - } else if (mountProvider.hasCapability(MOUNT_TO_EXISTING_DIR) || mountProvider.hasCapability(MOUNT_WITHIN_EXISTING_PARENT) || mountProvider.hasCapability(MOUNT_AS_DRIVE_LETTER)) { - // TODO: move the mount point away in case of MOUNT_WITHIN_EXISTING_PARENT? - builder.setMountpoint(userChosenMountPoint); - } - - return builder; - } - public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, MountFailedException { if (cryptoFileSystem.get() != null) { throw new IllegalStateException("Already unlocked."); @@ -207,8 +147,7 @@ public class Vault { try { cryptoFileSystem.set(fs); var rootPath = fs.getRootDirectories().iterator().next(); - var supportsForcedUnmount = mountService.getValue().hasCapability(MountCapability.UNMOUNT_FORCED); - var mountHandle = new MountHandle(prepareMount(rootPath).mount(), supportsForcedUnmount); + var mountHandle = mounter.mount(vaultSettings, rootPath); success = this.mountHandle.compareAndSet(null, mountHandle); } finally { if (!success) { @@ -217,7 +156,6 @@ public class Vault { } } - public synchronized void lock(boolean forced) throws UnmountFailedException, IOException { var mountHandle = this.mountHandle.get(); if (mountHandle == null) { @@ -225,14 +163,15 @@ public class Vault { return; } - if (forced && mountHandle.supportsUnmountForced) { - mountHandle.mount.unmountForced(); + if (forced && mountHandle.supportsUnmountForced()) { + mountHandle.mountObj().unmountForced(); } else { - mountHandle.mount.unmount(); + mountHandle.mountObj().unmount(); } try { - mountHandle.mount.close(); + mountHandle.mountObj().close(); + mountHandle.specialCleanup().run(); } finally { destroyCryptoFileSystem(); } @@ -327,7 +266,7 @@ public class Vault { public Mountpoint getMountPoint() { var handle = mountHandle.get(); - return handle == null ? null : handle.mount.getMountpoint(); + return handle == null ? null : handle.mountObj().getMountpoint(); } public StringBinding displayablePathProperty() { @@ -375,35 +314,26 @@ public class Vault { return vaultSettings.path().getValue(); } - public boolean isHavingCustomMountFlags() { - return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get()); - } - - public ObservableValue defaultMountFlagsProperty() { - return defaultMountFlags; - } - - public String getDefaultMountFlags() { - return defaultMountFlags.getValue(); - } - - public String getEffectiveMountFlags() { - String mountFlags = vaultSettings.mountFlags().get(); - if (Strings.isNullOrEmpty(mountFlags)) { - return ""; //TODO: should the provider provide dem defaults?? - } else { - return mountFlags; + /** + * Gets from the cleartext path its ciphertext counterpart. + * The cleartext path has to start from the vault root (by starting with "/"). + * + * @return Local os path to the ciphertext resource + * @throws IOException if an I/O error occurs + */ + public Path getCiphertextPath(String cleartextPath) throws IOException { + if (!cleartextPath.startsWith("/")) { + throw new IllegalArgumentException("Input path must be absolute from vault root by starting with \"/\"."); } + var fs = cryptoFileSystem.get(); + var cryptoPath = fs.getPath(cleartextPath); + return fs.getCiphertextPath(cryptoPath); } public VaultConfigCache getVaultConfigCache() { return configCache; } - public void setCustomMountFlags(String mountFlags) { - vaultSettings.mountFlags().set(mountFlags); - } - public String getId() { return vaultSettings.getId(); } @@ -426,15 +356,12 @@ public class Vault { } } - - /* TODO: reactivate/ needed at all? public boolean supportsForcedUnmount() { - return volume.supportsForcedUnmount(); + var mh = mountHandle.get(); + if (mh == null) { + throw new IllegalStateException("Vault is not mounted"); + } + return mountHandle.get().supportsUnmountForced(); } - */ - - private record MountHandle(Mount mount, boolean supportsUnmountForced) { - - } } \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java index db35b5a11..0148686f3 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java @@ -176,7 +176,7 @@ public class CreateNewVaultPasswordController implements FxController { // 2. initialize vault: try { MasterkeyLoader loader = ignored -> masterkey.copy(); - CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).withKeyLoader(loader).build(); + CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(CryptorProvider.Scheme.SIV_GCM).withKeyLoader(loader).build(); CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID); // 3. write vault-internal readme file: diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 73e805273..eac58eb86 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -13,6 +13,7 @@ public enum FxmlFile { FORGET_PASSWORD("/fxml/forget_password.fxml"), // HEALTH_START("/fxml/health_start.fxml"), // HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), // + HUB_NO_KEYCHAIN("/fxml/hub_no_keychain.fxml"), // HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), // HUB_LICENSE_EXCEEDED("/fxml/hub_license_exceeded.fxml"), // HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index ea6ba00d3..997bfa41f 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -25,6 +25,7 @@ public enum FontAwesome5Icon { EYE_SLASH("\uF070"), // FAST_FORWARD("\uF050"), // FILE("\uF15B"), // + FILE_DOWNLOAD("\uF56D"), // FILE_IMPORT("\uF56F"), // FOLDER_OPEN("\uF07C"), // FUNNEL("\uF0B0"), // diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java index a23a5f1b3..5adcb243d 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java @@ -87,6 +87,13 @@ public abstract class HubKeyLoadingModule { @StringKey(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS) abstract KeyLoadingStrategy bindHubKeyLoadingStrategyToHubHttps(HubKeyLoadingStrategy strategy); + @Provides + @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) + @KeyLoadingScoped + static Scene provideHubNoKeychainScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.HUB_NO_KEYCHAIN); + } + @Provides @FxmlScene(FxmlFile.HUB_AUTH_FLOW) @KeyLoadingScoped @@ -136,6 +143,11 @@ public abstract class HubKeyLoadingModule { return fxmlLoaders.createScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE); } + @Binds + @IntoMap + @FxControllerKey(NoKeychainController.class) + abstract FxController bindNoKeychainController(NoKeychainController controller); + @Binds @IntoMap @FxControllerKey(AuthFlowController.class) diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java index dcb5722d2..d1a3742ae 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java @@ -3,6 +3,8 @@ package org.cryptomator.ui.keyloading.hub; import com.google.common.base.Preconditions; import com.nimbusds.jose.JWEObject; import dagger.Lazy; +import org.cryptomator.common.keychain.KeychainManager; +import org.cryptomator.common.keychain.NoKeychainAccessProviderException; import org.cryptomator.common.settings.DeviceKey; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; @@ -31,15 +33,19 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https"; private final Stage window; + private final KeychainManager keychainManager; private final Lazy authFlowScene; + private final Lazy noKeychainScene; private final CompletableFuture result; private final DeviceKey deviceKey; @Inject - public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, CompletableFuture result, DeviceKey deviceKey, @Named("windowTitle") String windowTitle) { + public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) { this.window = window; + this.keychainManager = keychainManager; window.setTitle(windowTitle); this.authFlowScene = authFlowScene; + this.noKeychainScene = noKeychainScene; this.result = result; this.deviceKey = deviceKey; } @@ -48,9 +54,16 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException { Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX)); try { - startAuthFlow(); + if (!keychainManager.isSupported()) { + throw new NoKeychainAccessProviderException(); + } + var keypair = deviceKey.get(); + showWindow(authFlowScene); var jwe = result.get(); - return JWEHelper.decrypt(jwe, deviceKey.get().getPrivate()); + return JWEHelper.decrypt(jwe, keypair.getPrivate()); + } catch (NoKeychainAccessProviderException e) { + showWindow(noKeychainScene); + throw new UnlockCancelledException("Unlock canceled due to missing prerequisites", e); } catch (DeviceKey.DeviceKeyRetrievalException e) { throw new MasterkeyLoadingFailedException("Failed to load keypair", e); } catch (CancellationException e) { @@ -63,9 +76,9 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { } } - private void startAuthFlow() { + private void showWindow(Lazy scene) { Platform.runLater(() -> { - window.setScene(authFlowScene.get()); + window.setScene(scene.get()); window.show(); Window owner = window.getOwner(); if (owner != null) { diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/NoKeychainController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/NoKeychainController.java new file mode 100644 index 000000000..118152e1f --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/NoKeychainController.java @@ -0,0 +1,31 @@ +package org.cryptomator.ui.keyloading.hub; + +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.fxapp.FxApplicationWindows; +import org.cryptomator.ui.keyloading.KeyLoading; +import org.cryptomator.ui.preferences.SelectedPreferencesTab; + +import javax.inject.Inject; +import javafx.stage.Stage; + +public class NoKeychainController implements FxController { + + private final Stage window; + private final FxApplicationWindows appWindows; + + @Inject + public NoKeychainController(@KeyLoading Stage window, FxApplicationWindows appWindows) { + this.window = window; + this.appWindows = appWindows; + } + + + public void cancel() { + window.close(); + } + + public void openPreferences() { + appWindows.showPreferencesWindow(SelectedPreferencesTab.GENERAL); + window.close(); + } +} diff --git a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java index 9b653816a..15cf119be 100644 --- a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java +++ b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java @@ -53,7 +53,7 @@ public class LockForcedController implements FxController { } public boolean isForceSupported() { - return false;//vault.supportsForcedUnmount(); TODO + return vault.supportsForcedUnmount(); } } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java index c81aff125..b2c912834 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java @@ -3,33 +3,17 @@ package org.cryptomator.ui.mainwindow; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultListManager; -import org.cryptomator.cryptofs.CryptoFileSystemProvider; -import org.cryptomator.cryptofs.DirStructure; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.beans.Observable; -import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; -import javafx.scene.input.DragEvent; -import javafx.scene.input.TransferMode; import javafx.scene.layout.StackPane; import javafx.stage.Stage; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.Set; -import java.util.stream.Collectors; - -import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_EXT; -import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; -import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; @MainWindowScoped public class MainWindowController implements FxController { @@ -37,28 +21,19 @@ public class MainWindowController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(MainWindowController.class); private final Stage window; - private final VaultListManager vaultListManager; private final ReadOnlyObjectProperty selectedVault; - private final WrongFileAlertComponent.Builder wrongFileAlert; - private final BooleanProperty draggingOver = new SimpleBooleanProperty(); - private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty(); + public StackPane root; @Inject - public MainWindowController(@MainWindow Stage window, VaultListManager vaultListManager, ObjectProperty selectedVault, WrongFileAlertComponent.Builder wrongFileAlert) { + public MainWindowController(@MainWindow Stage window, ObjectProperty selectedVault) { this.window = window; - this.vaultListManager = vaultListManager; this.selectedVault = selectedVault; - this.wrongFileAlert = wrongFileAlert; } @FXML public void initialize() { LOG.trace("init MainWindowController"); - root.setOnDragEntered(this::handleDragEvent); - root.setOnDragOver(this::handleDragEvent); - root.setOnDragDropped(this::handleDragEvent); - root.setOnDragExited(this::handleDragEvent); if (SystemUtils.IS_OS_WINDOWS) { root.getStyleClass().add("os-windows"); } @@ -72,65 +47,4 @@ public class MainWindowController implements FxController { } } - private void handleDragEvent(DragEvent event) { - if (DragEvent.DRAG_ENTERED.equals(event.getEventType()) && event.getGestureSource() == null) { - draggingOver.set(true); - } else if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) { - event.acceptTransferModes(TransferMode.ANY); - draggingVaultOver.set(event.getDragboard().getFiles().stream().map(File::toPath).anyMatch(this::containsVault)); - } else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) { - Set vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).filter(this::containsVault).collect(Collectors.toSet()); - if (vaultPaths.isEmpty()) { - wrongFileAlert.build().showWrongFileAlertWindow(); - } else { - vaultPaths.forEach(this::addVault); - } - event.setDropCompleted(!vaultPaths.isEmpty()); - event.consume(); - } else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) { - draggingOver.set(false); - draggingVaultOver.set(false); - } - } - - private boolean containsVault(Path path) { - try { - if (path.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) { - path = path.getParent(); - } - return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED; - } catch (IOException e) { - return false; - } - } - - private void addVault(Path pathToVault) { - try { - if (pathToVault.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) { - vaultListManager.add(pathToVault.getParent()); - } else { - vaultListManager.add(pathToVault); - } - } catch (IOException e) { - LOG.debug("Not a vault: {}", pathToVault); - } - } - - /* Getter/Setter */ - - public BooleanProperty draggingOverProperty() { - return draggingOver; - } - - public boolean isDraggingOver() { - return draggingOver.get(); - } - - public BooleanProperty draggingVaultOverProperty() { - return draggingVaultOver; - } - - public boolean isDraggingVaultOver() { - return draggingVaultOver.get(); - } } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java index c86ea1929..1e5185c06 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java @@ -1,54 +1,118 @@ 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.common.vaults.VaultState; import org.cryptomator.integrations.mount.Mountpoint; +import org.cryptomator.integrations.revealpath.RevealFailedException; +import org.cryptomator.integrations.revealpath.RevealPathService; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.cryptomator.ui.stats.VaultStatisticsComponent; +import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanBinding; -import javafx.beans.binding.StringBinding; +import javafx.application.Platform; +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; +import javafx.stage.FileChooser; import javafx.stage.Stage; -import java.net.URI; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; @MainWindowScoped 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; + private final WrongFileAlertComponent.Builder wrongFileAlert; private final Stage mainWindow; + private final ResourceBundle resourceBundle; private final LoadingCache vaultStats; private final VaultStatisticsComponent.Builder vaultStatsBuilder; - private final ObservableValue mountPoint; private final ObservableValue accessibleViaPath; private final ObservableValue accessibleViaUri; - private final ObservableValue mountUri; + private final ObservableValue mountPoint; + private final BooleanProperty draggingOver = new SimpleBooleanProperty(); + private final BooleanProperty ciphertextPathsCopied = new SimpleBooleanProperty(); + + //FXML + public Button dropZone; @Inject - public VaultDetailUnlockedController(ObjectProperty vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) { + public VaultDetailUnlockedController(ObjectProperty vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, WrongFileAlertComponent.Builder wrongFileAlert, @MainWindow Stage mainWindow, ResourceBundle resourceBundle) { this.vault = vault; this.appWindows = appWindows; this.vaultService = vaultService; + this.wrongFileAlert = wrongFileAlert; this.mainWindow = mainWindow; + this.resourceBundle = resourceBundle; this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats)); this.vaultStatsBuilder = vaultStatsBuilder; - this.mountPoint = vault.flatMap(Vault::mountPointProperty); - this.accessibleViaPath = mountPoint.map(m -> m instanceof Mountpoint.WithPath).orElse(false); - this.accessibleViaUri = mountPoint.map(m -> m instanceof Mountpoint.WithUri).orElse(false); - this.mountUri = mountPoint.map(Mountpoint::uri).map(URI::toASCIIString).orElse(""); + 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() { + dropZone.setOnDragEntered(this::handleDragEvent); + dropZone.setOnDragOver(this::handleDragEvent); + dropZone.setOnDragDropped(this::handleDragEvent); + dropZone.setOnDragExited(this::handleDragEvent); + + EasyBind.includeWhen(dropZone.getStyleClass(), ACTIVE_CLASS, draggingOver); + } + + private void handleDragEvent(DragEvent event) { + if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) { + event.acceptTransferModes(TransferMode.LINK); + draggingOver.set(true); + } else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) { + List ciphertextPaths = event.getDragboard().getFiles().stream().map(File::toPath).map(this::getCiphertextPath).flatMap(Optional::stream).toList(); + if (ciphertextPaths.isEmpty()) { + wrongFileAlert.build().showWrongFileAlertWindow(); + } else { + revealOrCopyPaths(ciphertextPaths); + } + event.setDropCompleted(!ciphertextPaths.isEmpty()); + event.consume(); + } else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) { + draggingOver.set(false); + } } private VaultStatisticsComponent buildVaultStats(Vault vault) { @@ -62,7 +126,9 @@ public class VaultDetailUnlockedController implements FxController { @FXML public void copyMountUri() { - // TODO + ClipboardContent clipboardContent = new ClipboardContent(); + clipboardContent.putString(mountPoint.getValue()); + Clipboard.getSystemClipboard().setContent(clipboardContent); } @FXML @@ -75,6 +141,77 @@ public class VaultDetailUnlockedController implements FxController { vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow(); } + @FXML + public void chooseFileAndReveal() { + Preconditions.checkState(accessibleViaPath.getValue()); + var fileChooser = new FileChooser(); + fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.filePickerTitle")); + fileChooser.setInitialDirectory(Path.of(mountPoint.getValue()).toFile()); + var cleartextFile = fileChooser.showOpenDialog(mainWindow); + if (cleartextFile != null) { + var ciphertextPaths = getCiphertextPath(cleartextFile.toPath()).stream().toList(); + revealOrCopyPaths(ciphertextPaths); + } + } + + private boolean startsWithVaultAccessPoint(Path path) { + return path.startsWith(Path.of(mountPoint.getValue())); + } + + private Optional getCiphertextPath(Path path) { + if (!startsWithVaultAccessPoint(path)) { + LOG.debug("Path does not start with access point of selected vault: {}", path); + return Optional.empty(); + } + try { + var accessPoint = mountPoint.getValue(); + var cleartextPath = path.toString().substring(accessPoint.length()); + if (!cleartextPath.startsWith("/")) { + cleartextPath = "/" + cleartextPath; + } + return Optional.of(vault.get().getCiphertextPath(cleartextPath)); + } catch (IOException e) { + LOG.warn("Unable to get ciphertext path from path: {}", path); + return Optional.empty(); + } + } + + private void revealOrCopyPaths(List paths) { + if (!revealPaths(paths)) { + LOG.warn("No service provider to reveal files found."); + copyPathsToClipboard(paths); + } + } + + /** + * Reveals the paths over the {@link RevealPathService} in the file system + * + * @param paths List of Paths to reveal + * @return true, if at least one service provider was present, false otherwise + */ + private boolean revealPaths(List paths) { + return RevealPathService.get().findAny().map(s -> { + paths.forEach(path -> { + try { + s.reveal(path); + } catch (RevealFailedException e) { + LOG.error("Revealing ciphertext file failed.", e); + } + }); + return true; + }).orElse(false); + } + + private void copyPathsToClipboard(List paths) { + StringBuilder clipboardString = new StringBuilder(); + paths.forEach(p -> clipboardString.append(p.toString()).append("\n")); + Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, clipboardString.toString())); + ciphertextPathsCopied.setValue(true); + CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> { + ciphertextPathsCopied.set(false); + }); + } + /* Getter/Setter */ public ReadOnlyObjectProperty vaultProperty() { @@ -101,13 +238,19 @@ public class VaultDetailUnlockedController implements FxController { return accessibleViaUri.getValue(); } - public ObservableValue mountUriProperty() { - return mountUri; + public ObservableValue mountPointProperty() { + return mountPoint; } - public String getMountUri() { - return mountUri.getValue(); + public String getMountPoint() { + return mountPoint.getValue(); } + public BooleanProperty ciphertextPathsCopiedProperty() { + return ciphertextPathsCopied; + } + public boolean isCiphertextPathsCopied() { + return ciphertextPathsCopied.get(); + } } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index adb9a961b..f0aadfdfc 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java @@ -3,26 +3,43 @@ package org.cryptomator.ui.mainwindow; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultListManager; +import org.cryptomator.cryptofs.CryptoFileSystemProvider; +import org.cryptomator.cryptofs.DirStructure; import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.removevault.RemoveVaultComponent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ListView; import javafx.scene.input.ContextMenuEvent; +import javafx.scene.input.DragEvent; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; +import javafx.scene.input.TransferMode; +import javafx.scene.layout.StackPane; import javafx.stage.Stage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; import java.util.EnumSet; +import java.util.Set; +import java.util.stream.Collectors; +import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_EXT; +import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; import static org.cryptomator.common.vaults.VaultState.Value.ERROR; import static org.cryptomator.common.vaults.VaultState.Value.LOCKED; import static org.cryptomator.common.vaults.VaultState.Value.MISSING; @@ -31,6 +48,7 @@ import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION; @MainWindowScoped public class VaultListController implements FxController { + private static final Logger LOG = LoggerFactory.getLogger(VaultListController.class); private final Stage mainWindow; private final ObservableList vaults; @@ -39,17 +57,21 @@ public class VaultListController implements FxController { private final AddVaultWizardComponent.Builder addVaultWizard; private final BooleanBinding emptyVaultList; private final RemoveVaultComponent.Builder removeVaultDialogue; + private final VaultListManager vaultListManager; + private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty(); public ListView vaultList; + public StackPane root; @Inject - VaultListController(@MainWindow Stage mainWindow, ObservableList vaults, ObjectProperty selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue) { + VaultListController(@MainWindow Stage mainWindow, ObservableList vaults, ObjectProperty selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue, VaultListManager vaultListManager) { this.mainWindow = mainWindow; this.vaults = vaults; this.selectedVault = selectedVault; this.cellFactory = cellFactory; this.addVaultWizard = addVaultWizard; this.removeVaultDialogue = removeVaultDialogue; + this.vaultListManager = vaultListManager; this.emptyVaultList = Bindings.isEmpty(vaults); @@ -100,6 +122,11 @@ public class VaultListController implements FxController { keyEvent.consume(); } }); + + root.setOnDragEntered(this::handleDragEvent); + root.setOnDragOver(this::handleDragEvent); + root.setOnDragDropped(this::handleDragEvent); + root.setOnDragExited(this::handleDragEvent); } private void deselect(MouseEvent released) { @@ -128,6 +155,47 @@ public class VaultListController implements FxController { } } + private void handleDragEvent(DragEvent event) { + if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) { + draggingVaultOver.set(event.getDragboard().getFiles().stream().map(File::toPath).anyMatch(this::containsVault)); + if (draggingVaultOver.get()) { + event.acceptTransferModes(TransferMode.ANY); + } + } else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) { + Set vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).filter(this::containsVault).collect(Collectors.toSet()); + if (!vaultPaths.isEmpty()) { + vaultPaths.forEach(this::addVault); + } + event.setDropCompleted(!vaultPaths.isEmpty()); + event.consume(); + } else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) { + draggingVaultOver.set(false); + } + } + + private boolean containsVault(Path path) { + try { + if (path.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) { + path = path.getParent(); + } + return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED; + } catch (IOException e) { + return false; + } + } + + private void addVault(Path pathToVault) { + try { + if (pathToVault.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) { + vaultListManager.add(pathToVault.getParent()); + } else { + vaultListManager.add(pathToVault); + } + } catch (IOException e) { + LOG.debug("Not a vault: {}", pathToVault); + } + } + // Getter and Setter public BooleanBinding emptyVaultListProperty() { @@ -138,4 +206,13 @@ public class VaultListController implements FxController { return emptyVaultList.get(); } + public BooleanProperty draggingVaultOverProperty() { + return draggingVaultOver; + } + + public boolean isDraggingVaultOver() { + return draggingVaultOver.get(); + } + + } diff --git a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java index ac4903d11..00e680d2d 100644 --- a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java @@ -1,5 +1,6 @@ package org.cryptomator.ui.preferences; +import org.cryptomator.common.ObservableUtil; import org.cryptomator.common.settings.Settings; import org.cryptomator.integrations.mount.MountCapability; import org.cryptomator.integrations.mount.MountService; @@ -14,39 +15,54 @@ 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 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, List mountProviders, ObservableValue selectedMountService) { + VolumePreferencesController(Settings settings, List mountProviders, ResourceBundle resourceBundle) { this.settings = settings; this.mountProviders = mountProviders; - this.selectedMountService = selectedMountService; + 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().add(null); volumeTypeChoiceBox.getItems().addAll(mountProviders); volumeTypeChoiceBox.setConverter(new MountServiceConverter()); - volumeTypeChoiceBox.getSelectionModel().select(selectedMountService.getValue()); - volumeTypeChoiceBox.valueProperty().addListener((observableValue, oldProvide, newProvider) -> settings.mountService().set(newProvider.getClass().getName())); + 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); + }); 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 validateLoopbackPort() { @@ -75,13 +91,49 @@ public class VolumePreferencesController implements FxController { return loopbackPortSupported.get(); } + public ObservableValue readonlySupportedProperty() { + return readonlySupported; + } + + public boolean isReadonlySupported() { + return readonlySupported.getValue(); + } + + public ObservableValue mountToDirSupportedProperty() { + return mountToDirSupported; + } + + 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 static class MountServiceConverter extends StringConverter { + private class MountServiceConverter extends StringConverter { @Override public String toString(MountService provider) { - return provider== null? "None" : provider.displayName(); //TODO: adjust message + if (provider == null) { + return resourceBundle.getString("preferences.volume.type.automatic"); + } else { + return provider.displayName(); + } } @Override @@ -89,6 +141,4 @@ public class VolumePreferencesController implements FxController { 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 36553e56c..704ffa9ab 100644 --- a/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java +++ b/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java @@ -39,8 +39,8 @@ public class AwtTrayMenuController implements TrayMenuController { } @Override - public void showTrayIcon(byte[] rawImageData, Runnable defaultAction, String tooltip) throws TrayMenuException { - var image = Toolkit.getDefaultToolkit().createImage(rawImageData); + public void showTrayIcon(byte[] imageData, Runnable defaultAction, String tooltip) throws TrayMenuException { + var image = Toolkit.getDefaultToolkit().createImage(imageData); trayIcon = new TrayIcon(image, tooltip, menu); trayIcon.setImageAutoSize(true); @@ -56,6 +56,15 @@ public class AwtTrayMenuController implements TrayMenuController { } } + @Override + public void updateTrayIcon(byte[] imageData) { + if (trayIcon == null) { + throw new IllegalStateException("Failed to update the icon as it has not yet been added"); + } + var image = Toolkit.getDefaultToolkit().createImage(imageData); + trayIcon.setImage(image); + } + @Override public void updateTrayMenu(List items) { menu.removeAll(); @@ -67,7 +76,7 @@ public class AwtTrayMenuController implements TrayMenuController { Preconditions.checkNotNull(this.trayIcon); this.trayIcon.addMouseListener(new MouseAdapter() { @Override - public void mouseClicked(MouseEvent e) { + public void mousePressed(MouseEvent e) { listener.run(); } }); diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java index e96446143..fd0a0d987 100644 --- a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java +++ b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java @@ -33,7 +33,9 @@ public class TrayMenuBuilder { private static final Logger LOG = LoggerFactory.getLogger(TrayMenuBuilder.class); private static final String TRAY_ICON_MAC = "/img/tray_icon_mac@2x.png"; - private static final String TRAY_ICON = "/img/window_icon_32.png"; + private static final String TRAY_ICON_UNLOCKED_MAC = "/img/tray_icon_unlocked_mac@2x.png"; + private static final String TRAY_ICON = "/img/tray_icon.png"; + private static final String TRAY_ICON_UNLOCKED = "/img/tray_icon_unlocked.png"; private final ResourceBundle resourceBundle; private final VaultService vaultService; @@ -62,8 +64,8 @@ public class TrayMenuBuilder { v.displayNameProperty().addListener(this::vaultListChanged); }); - try (var image = getClass().getResourceAsStream(SystemUtils.IS_OS_MAC_OSX ? TRAY_ICON_MAC : TRAY_ICON)) { - trayMenu.showTrayIcon(image.readAllBytes(), this::showMainWindow, "Cryptomator"); + try { + trayMenu.showTrayIcon(getAppropriateTrayIconImage(), this::showMainWindow, "Cryptomator"); trayMenu.onBeforeOpenMenu(() -> { for (Vault vault : vaults) { VaultListManager.redetermineVaultState(vault); @@ -71,8 +73,6 @@ public class TrayMenuBuilder { }); rebuildMenu(); initialized = true; - } catch (IOException e) { - throw new UncheckedIOException("Failed to load embedded resource", e); } catch (TrayMenuException e) { LOG.error("Adding tray icon failed", e); } @@ -84,6 +84,7 @@ public class TrayMenuBuilder { private void vaultListChanged(@SuppressWarnings("unused") Observable observable) { assert Platform.isFxApplicationThread(); + trayMenu.updateTrayIcon(getAppropriateTrayIconImage()); rebuildMenu(); } @@ -154,4 +155,22 @@ public class TrayMenuBuilder { appWindows.showPreferencesWindow(SelectedPreferencesTab.ANY); } + private byte[] getAppropriateTrayIconImage() { + boolean isAnyVaultUnlocked = vaults.stream().anyMatch(Vault::isUnlocked); + + String resourceName; + if (SystemUtils.IS_OS_MAC_OSX) { + resourceName = isAnyVaultUnlocked ? TRAY_ICON_UNLOCKED_MAC : TRAY_ICON_MAC; + } else { + resourceName = isAnyVaultUnlocked ? TRAY_ICON_UNLOCKED : TRAY_ICON; + } + + try (var image = getClass().getResourceAsStream(resourceName)) { + assert image != null; + return image.readAllBytes(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load tray icon image: " + resourceName, e); + } + } + } diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java b/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java index 928953c9e..aeb4a9166 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java @@ -1,11 +1,18 @@ package org.cryptomator.ui.unlock; +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 @@ -13,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 @@ -25,4 +53,10 @@ public class UnlockInvalidMountPointController implements FxController { window.close(); } + @FXML + public void closeAndOpenPreferences() { + appWindows.showPreferencesWindow(SelectedPreferencesTab.VOLUME); + window.close(); + } + } \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index 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 9c47aa281..985f1b471 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -2,6 +2,7 @@ package org.cryptomator.ui.unlock; import com.google.common.base.Throwables; import dagger.Lazy; +import org.cryptomator.common.mount.IllegalMountPointException; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultState; import org.cryptomator.cryptolib.api.CryptoException; @@ -20,6 +21,7 @@ import javafx.concurrent.Task; import javafx.scene.Scene; import javafx.stage.Stage; import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; /** * A multi-step task that consists of background activities as well as user interaction. @@ -38,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; @@ -48,6 +51,7 @@ public class UnlockWorkflow extends Task { this.invalidMountPointScene = invalidMountPointScene; this.appWindows = appWindows; this.keyLoadingStrategy = keyLoadingStrategy; + this.unlockFailedException = unlockFailedException; } @Override @@ -67,13 +71,15 @@ public class UnlockWorkflow extends Task { } 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); } } - private void showInvalidMountPointScene() { + private void handleIllegalMountPointError(IllegalMountPointException impe) { Platform.runLater(() -> { + unlockFailedException.set(impe); window.setScene(invalidMountPointScene.get()); window.show(); }); @@ -107,7 +113,11 @@ public class UnlockWorkflow extends Task { protected void failed() { LOG.info("Unlock of '{}' failed.", vault.getDisplayName()); Throwable throwable = super.getException(); - handleGenericError(throwable); + if(throwable instanceof IllegalMountPointException impe) { + handleIllegalMountPointError(impe); + } else { + 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 8c28b1a5d..502d4312a 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java @@ -1,11 +1,14 @@ package org.cryptomator.ui.vaultoptions; -import org.cryptomator.common.Environment; +import com.google.common.base.Strings; +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.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 javax.inject.Inject; import javafx.beans.value.ObservableValue; @@ -30,16 +33,17 @@ import java.util.Set; public class MountOptionsController implements FxController { private final Stage window; - private final Vault vault; + 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 driveLetter; private final ObservableValue directoryPath; + private final FxApplicationWindows applicationWindows; //-- FXML objects -- @@ -54,59 +58,75 @@ public class MountOptionsController implements FxController { public ChoiceBox driveLetterSelection; @Inject - MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue mountService, 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.vaultSettings = vault.getVaultSettings(); this.windowsDriveLetters = windowsDriveLetters; this.resourceBundle = resourceBundle; - this.mountpointDirSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR) || s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT)); - this.mountpointDriveLetterSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER)); - this.mountFlagsSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS)); - this.readOnlySupported = mountService.map(s -> s.hasCapability(MountCapability.READ_ONLY)); - this.driveLetter = vault.getVaultSettings().mountPoint().map(p -> isDriveLetter(p) ? p : null); + 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()); + readOnlyCheckbox.selectedProperty().bindBidirectional(vaultSettings.usesReadOnlyMode()); // custom mount flags: mountFlagsField.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().not()); - customMountFlagsCheckbox.setSelected(vault.isHavingCustomMountFlags()); + customMountFlagsCheckbox.setSelected(!Strings.isNullOrEmpty(vaultSettings.mountFlags().getValue())); + toggleUseCustomMountFlags(); //driveLetter choice box driveLetterSelection.getItems().addAll(windowsDriveLetters.getAll()); driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle)); - driveLetterSelection.setOnShowing(event -> driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle))); //TODO: does this work? //mountPoint toggle group - var mountPoint = vault.getVaultSettings().getMountPoint(); + 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); - } else if (driveLetterSelection.getValue() == null) { + driveLetterSelection.valueProperty().bindBidirectional(vaultSettings.mountPoint()); + } else { //prepare and select dir mountPointToggleGroup.selectToggle(mountPointDirBtn); } mountPointToggleGroup.selectedToggleProperty().addListener(this::selectedToggleChanged); } + @FXML + public void openVolumePreferences() { + applicationWindows.showPreferencesWindow(SelectedPreferencesTab.VOLUME); + } + @FXML public void toggleUseCustomMountFlags() { if (customMountFlagsCheckbox.isSelected()) { readOnlyCheckbox.setSelected(false); // to prevent invalid states mountFlagsField.textProperty().unbind(); - vault.setCustomMountFlags(vault.defaultMountFlagsProperty().getValue()); - mountFlagsField.textProperty().bindBidirectional(vault.getVaultSettings().mountFlags()); + var mountFlags = vaultSettings.mountFlags().get(); + if (mountFlags == null || mountFlags.isBlank()) { + vaultSettings.mountFlags().set(defaultMountFlags.getValue()); + } + mountFlagsField.textProperty().bindBidirectional(vaultSettings.mountFlags()); } else { - mountFlagsField.textProperty().unbindBidirectional(vault.getVaultSettings().mountFlags()); - vault.setCustomMountFlags(null); - mountFlagsField.textProperty().bind(vault.defaultMountFlagsProperty()); + mountFlagsField.textProperty().unbindBidirectional(vaultSettings.mountFlags()); + vaultSettings.mountFlags().set(null); + mountFlagsField.textProperty().bind(defaultMountFlags); } } @@ -114,7 +134,7 @@ public class MountOptionsController implements FxController { public void chooseCustomMountPoint() { try { Path chosenPath = chooseCustomMountPointInternal(); - vault.getVaultSettings().mountPoint().set(chosenPath); + vaultSettings.mountPoint().set(chosenPath); } catch (NoDirSelectedException e) { //no-op } @@ -131,7 +151,7 @@ public class MountOptionsController implements FxController { DirectoryChooser directoryChooser = new DirectoryChooser(); directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle")); try { - var mp = vault.getVaultSettings().mountPoint().get(); + var mp = vaultSettings.mountPoint().get(); var initialDir = mp != null && !isDriveLetter(mp) ? mp : Path.of(System.getProperty("user.home")); if (Files.isDirectory(initialDir)) { @@ -149,27 +169,30 @@ public class MountOptionsController implements FxController { } private void selectedToggleChanged(ObservableValue observable, Toggle oldToggle, Toggle newToggle) { - Path mountPointToBe = null; - try { - //Remark: the mountpoint corresponding to the newToggle must be null, otherwise it would not be new! - if (mountPointDriveLetterBtn.equals(newToggle)) { - mountPointToBe = driveLetterSelection.getItems().get(0); - } else if (mountPointDirBtn.equals(newToggle)) { - mountPointToBe = chooseCustomMountPointInternal(); - } - vault.getVaultSettings().mountPoint().set(mountPointToBe); - } catch (NoDirSelectedException e) { - if (!mountPointDirBtn.equals(oldToggle)) { - mountPointToggleGroup.selectToggle(oldToggle); - + //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); } } private boolean isDriveLetter(Path mountPoint) { if (mountPoint != null) { var s = mountPoint.toString(); - return s.length() == 3 && mountPoint.toString().endsWith(":\\"); + return s.length() == 3 && s.endsWith(":\\"); } return false; } @@ -237,21 +260,13 @@ public class MountOptionsController implements FxController { } public ObservableValue readOnlySupportedProperty() { - return mountpointDriveLetterSupported; + return readOnlySupported; } public boolean isReadOnlySupported() { return readOnlySupported.getValue(); } - public ObservableValue driveLetterProperty() { - return driveLetter; - } - - public Path getDriveLetter() { - return driveLetter.getValue(); - } - public ObservableValue directoryPathProperty() { return directoryPath; } @@ -259,5 +274,4 @@ public class MountOptionsController implements FxController { public String getDirectoryPath() { return directoryPath.getValue(); } - } diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css index 86467bb1a..5f0877842 100644 --- a/src/main/resources/css/dark_theme.css +++ b/src/main/resources/css/dark_theme.css @@ -204,16 +204,6 @@ -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 2, 0, 0, 0); } -.main-window .drag-n-drop-indicator { - -fx-border-color: SECONDARY; - -fx-border-width: 3px; -} - -.main-window .drag-n-drop-indicator .drag-n-drop-header { - -fx-background-color: SECONDARY; - -fx-padding: 3px; -} - /******************************************************************************* * * * TabPane * @@ -884,3 +874,51 @@ -fx-fill: linear-gradient(to bottom, PRIMARY, transparent); -fx-stroke: transparent; } + +/******************************************************************************* + * * + * Drag and Drop * + * * + ******************************************************************************/ + +.drag-n-drop-border { + -fx-border-color: SECONDARY; + -fx-border-width: 3px; +} + +.button.drag-n-drop { + -fx-background-color: CONTROL_BG_NORMAL; + -fx-background-insets: 0; + -fx-padding: 1.4em 1em 1.4em 1em; + -fx-text-fill: TEXT_FILL_MUTED; + -fx-font-size: 0.8em; + -fx-border-color: CONTROL_BORDER_NORMAL; + -fx-border-radius: 4px; + -fx-border-style: dashed inside; + -fx-border-width: 1px; +} + +.button.drag-n-drop:focused { + -fx-border-color: CONTROL_BORDER_FOCUSED; +} + +.button.drag-n-drop:armed { + -fx-background-color: CONTROL_BG_ARMED; +} + +.button.drag-n-drop.active { + -fx-border-color: SECONDARY; + -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 ddc872eb2..decf64b64 100644 --- a/src/main/resources/css/light_theme.css +++ b/src/main/resources/css/light_theme.css @@ -203,16 +203,6 @@ -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 2, 0, 0, 0); } -.main-window .drag-n-drop-indicator { - -fx-border-color: SECONDARY; - -fx-border-width: 3px; -} - -.main-window .drag-n-drop-indicator .drag-n-drop-header { - -fx-background-color: SECONDARY; - -fx-padding: 3px; -} - /******************************************************************************* * * * TabPane * @@ -883,3 +873,51 @@ -fx-fill: linear-gradient(to bottom, PRIMARY, transparent); -fx-stroke: transparent; } + +/******************************************************************************* + * * + * Drag and Drop * + * * + ******************************************************************************/ + +.drag-n-drop-border { + -fx-border-color: SECONDARY; + -fx-border-width: 3px; +} + +.button.drag-n-drop { + -fx-background-color: CONTROL_BG_NORMAL; + -fx-background-insets: 0; + -fx-padding: 1.4em 1em 1.4em 1em; + -fx-text-fill: TEXT_FILL_MUTED; + -fx-font-size: 0.8em; + -fx-border-color: CONTROL_BORDER_NORMAL; + -fx-border-radius: 4px; + -fx-border-style: dashed inside; + -fx-border-width: 1px; +} + +.button.drag-n-drop:focused { + -fx-border-color: CONTROL_BORDER_FOCUSED; +} + +.button.drag-n-drop:armed { + -fx-background-color: CONTROL_BG_ARMED; +} + +.button.drag-n-drop.active { + -fx-border-color: SECONDARY; + -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/hub_no_keychain.fxml b/src/main/resources/fxml/hub_no_keychain.fxml new file mode 100644 index 000000000..031eac956 --- /dev/null +++ b/src/main/resources/fxml/hub_no_keychain.fxml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/vault_options_mount.fxml b/src/main/resources/fxml/vault_options_mount.fxml index 5119724c2..5e9e0c19f 100644 --- a/src/main/resources/fxml/vault_options_mount.fxml +++ b/src/main/resources/fxml/vault_options_mount.fxml @@ -5,12 +5,15 @@ + + + - diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index d9a8f5260..fbee45826 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -124,12 +124,14 @@ unlock.success.description=Content in vault "%s" is now accessible over its moun unlock.success.rememberChoice=Remember my choice, don't ask again unlock.success.revealBtn=Reveal Drive ## Failure -unlock.error.message=Unable to unlock vault -### Invalid Mount Point -unlock.error.invalidMountPoint.notExisting=Mount point "%s" is not a directory, not empty or does not exist. -unlock.error.invalidMountPoint.existing=Mount point "%s" already exists or parent folder is missing. -unlock.error.invalidMountPoint.driveLetterOccupied=Drive Letter "%s" is already in use. +unlock.error.customPath.message=Unable to mount vault to custom path +unlock.error.customPath.description.notSupported=If you wish to keep using the custom path, please go to the preferences and select a volume type that supports it. Otherwise, go to the vault options and choose a supported mount point. +unlock.error.customPath.description.notExists=The custom mount path does not exist. Either create it in your local filesystem or change it in the vault options. +unlock.error.customPath.description.generic=You have selected a custom mount path for this vault, but using it failed with the message: %s ## Hub +hub.noKeychain.message=Unable to access device key +hub.noKeychain.description=In order to unlock Hub vaults, a device key is required, which is secured using a keychain. To proceed, enable “%s” and select a keychain in the preferences. +hub.noKeychain.openBtn=Open Preferences ### Waiting hub.auth.message=Waiting for authentication… hub.auth.description=You should automatically be redirected to the login page. @@ -170,8 +172,13 @@ lock.fail.description=Vault "%s" could not be locked. Ensure unsaved work is sav # Migration migration.title=Upgrade Vault ## Start -migration.start.prompt=Your vault "%s" needs to be updated to a newer format. Before proceeding, make sure there is no pending synchronization affecting this vault. -migration.start.confirm=Yes, my vault is fully synced +migration.start.header=Upgrade Vault +migration.start.text=In order to open your vault "%s" in this new version of Cryptomator, the vault needs to be upgraded to a newer format. Before doing this, you should know the following: +migration.start.remarkUndone=This upgrade cannot be undone. +migration.start.remarkVersions=Older versions of Cryptomator will not be able to open the upgraded vault. +migration.start.remarkCanRun=You must be sure that every device from which you access the vault can run this version of Cryptomator. +migration.start.remarkSynced=You must be sure that your vault is fully synced on this device, and on your other devices, before upgrading it. +migration.start.confirm=I have read and understood the above information ## Run migration.run.enterPassword=Enter the password for "%s" migration.run.startMigrationBtn=Migrate Vault @@ -269,8 +276,15 @@ preferences.interface.showMinimizeButton=Show minimize button preferences.interface.showTrayIcon=Show tray icon (requires restart) ## Volume preferences.volume=Virtual Drive -preferences.volume.type=Volume Type +preferences.volume.type=Volume Type (requires restart) +preferences.volume.type.automatic=Automatic preferences.volume.tcp.port=TCP Port +preferences.volume.supportedFeatures=The chosen volume type supports the following features: +preferences.volume.feature.mountAuto=Automatic mount point selection +preferences.volume.feature.mountToDir=Custom directory as mount point +preferences.volume.feature.mountToDriveLetter=Drive letter as mount point +preferences.volume.feature.mountFlags=Custom mount options +preferences.volume.feature.readOnly=Read-only mount ## Updates preferences.updates=Updates preferences.updates.currentVersion=Current Version: %s @@ -329,9 +343,6 @@ main.minimizeBtn.tooltip=Minimize main.preferencesBtn.tooltip=Preferences main.debugModeEnabled.tooltip=Debug mode is enabled main.supporterCertificateMissing.tooltip=Please consider donating -## Drag 'n' Drop -main.dropZone.dropVault=Add this vault -main.dropZone.unknownDragboardContent=If you want to add a vault, drag it to this window ## Vault List main.vaultlist.emptyList.onboardingInstruction=Click here to add a vault main.vaultlist.contextMenu.remove=Remove… @@ -354,6 +365,7 @@ main.vaultDetail.passwordSavedInKeychain=Password saved main.vaultDetail.unlockedStatus=UNLOCKED main.vaultDetail.accessLocation=Your vault's contents are accessible here: main.vaultDetail.revealBtn=Reveal Drive +main.vaultDetail.copyUri=Copy URI main.vaultDetail.lockBtn=Lock main.vaultDetail.bytesPerSecondRead=Read: main.vaultDetail.bytesPerSecondWritten=Write: @@ -361,6 +373,10 @@ main.vaultDetail.throughput.idle=idle main.vaultDetail.throughput.kbps=%.1f kiB/s main.vaultDetail.throughput.mbps=%.1f MiB/s main.vaultDetail.stats=Vault Statistics +main.vaultDetail.locateEncryptedFileBtn=Locate Encrypted File +main.vaultDetail.locateEncryptedFileBtn.tooltip=Choose a file from your vault to locate its encrypted counterpart +main.vaultDetail.encryptedPathsCopied=Paths Copied to Clipboard! +main.vaultDetail.filePickerTitle=Select File Inside Vault ### Missing main.vaultDetail.missing.info=Cryptomator could not find a vault at this path. main.vaultDetail.missing.recheck=Recheck @@ -399,6 +415,8 @@ vaultOptions.general.startHealthCheckBtn=Start Health Check ## Mount vaultOptions.mount=Mounting +vaultOptions.mount.info=Options depend on the selected volume type. +vaultOptions.mount.linkToPreferences=Open virtual drive preferences vaultOptions.mount.readonly=Read-only vaultOptions.mount.customMountFlags=Custom mount flags vaultOptions.mount.winDriveLetterOccupied=occupied diff --git a/src/main/resources/img/tray_icon.png b/src/main/resources/img/tray_icon.png index 6fa661d3c..0d14953d3 100644 Binary files a/src/main/resources/img/tray_icon.png and b/src/main/resources/img/tray_icon.png differ diff --git a/src/main/resources/img/tray_icon_mac.png b/src/main/resources/img/tray_icon_mac.png deleted file mode 100755 index b0f2c7894..000000000 Binary files a/src/main/resources/img/tray_icon_mac.png and /dev/null differ diff --git a/src/main/resources/img/tray_icon_unlocked.png b/src/main/resources/img/tray_icon_unlocked.png new file mode 100644 index 000000000..a0079584b Binary files /dev/null and b/src/main/resources/img/tray_icon_unlocked.png differ diff --git a/src/main/resources/img/tray_icon_unlocked_mac@2x.png b/src/main/resources/img/tray_icon_unlocked_mac@2x.png new file mode 100644 index 000000000..9cd7edbd3 Binary files /dev/null and b/src/main/resources/img/tray_icon_unlocked_mac@2x.png differ diff --git a/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java b/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java index c69923329..2fcc0cf3b 100644 --- a/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java +++ b/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java @@ -2,21 +2,12 @@ package org.cryptomator.common.vaults; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; -import org.cryptomator.common.settings.VolumeImpl; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledOnOs; -import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; import org.mockito.Mockito; import javafx.beans.binding.Bindings; -import javafx.beans.binding.StringBinding; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import java.nio.file.Path; @@ -35,41 +26,4 @@ public class VaultModuleTest { System.setProperty("user.home", tmpDir.toString()); } - /* TODO: reactivate! - @Test - @DisplayName("provideDefaultMountFlags on Mac/FUSE") - @EnabledOnOs(OS.MAC) - public void testMacFuseDefaultMountFlags() { - Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.FUSE)); - - StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings); - - MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ovolname=\"TEST\"")); - MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ordonly")); - } - - @Test - @DisplayName("provideDefaultMountFlags on Linux/FUSE") - @EnabledOnOs(OS.LINUX) - public void testLinuxFuseDefaultMountFlags() { - Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.FUSE)); - - StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings); - - MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-oro")); - } - - @Test - @DisplayName("provideDefaultMountFlags on Windows/Dokany") - @EnabledOnOs(OS.WINDOWS) - public void testWinDokanyDefaultMountFlags() { - Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.DOKANY)); - - StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings); - - MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("--options CURRENT_SESSION,WRITE_PROTECTION")); - } - - */ - }