Merge branch 'develop' into release/1.7.0

# Conflicts:
#	dist/linux/debian/control
#	dist/linux/debian/cryptomator.sh
This commit is contained in:
Armin Schrenk
2023-01-26 13:47:46 +01:00
64 changed files with 1353 additions and 508 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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"

View File

@@ -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/"

View File

@@ -33,6 +33,7 @@ Cryptomator is provided free of charge as an open-source project despite the hig
<tr>
<td><a href="https://mowcapital.com/"><img src="https://cryptomator.org/img/sponsors/mowcapital.svg" alt="Mow Capital" height="40"></a></td>
<td><a href="https://www.easeus.com/"><img src="https://cryptomator.org/img/sponsors/easeus.png" alt="EaseUS" height="40"></a></td>
<td><a href="https://www.hassmann-it-forensik.de/"><img src="https://cryptomator.org/img/sponsors/hassmannitforensik.png" alt="Hassmann IT-Forensik" height="40"></a></td>
</tr>
</tbody>
</table>

View File

@@ -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 ""

View File

@@ -66,6 +66,7 @@
</content_rating>
<releases>
<release date="2022-12-14" version="1.6.17"/>
<release date="2022-12-06" version="1.6.16"/>
<release date="2022-10-06" version="1.6.15"/>
<release date="2022-08-31" version="1.6.14"/>

View File

@@ -4,11 +4,11 @@ Upstream-Contact: Cryptomator <info@cryptomator.org>
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+

View File

@@ -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\"" \

View File

@@ -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"

View File

@@ -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.\

Binary file not shown.

View File

@@ -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

View File

@@ -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

29
pom.xml
View File

@@ -24,18 +24,19 @@
<project.jdk.version>19</project.jdk.version>
<!-- Group IDs of jars that need to stay on the class path for now -->
<!-- Check progress on https://github.com/swiesend/secret-service/issues/31 to remove hypfvieh, swiesend and jnr -->
<nonModularGroupIds>com.github.jnr,org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh</nonModularGroupIds>
<!-- Check progress on https://github.com/swiesend/secret-service/issues/31 to remove swiesend -->
<nonModularGroupIds>org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava</nonModularGroupIds>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>2.5.3</cryptomator.cryptofs.version>
<cryptomator.integrations.version>1.2.0-beta2</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.1.2</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.1.2</cryptomator.integrations.mac.version>
<cryptomator.integrations.linux.version>1.1.0</cryptomator.integrations.linux.version>
<cryptomator.fuse.version>2.0.0-beta2</cryptomator.fuse.version>
<cryptomator.dokany.version>2.0.0-beta1</cryptomator.dokany.version>
<cryptomator.webdav.version>2.0.0-beta1</cryptomator.webdav.version>
<cryptomator.cryptolib.version>2.1.1</cryptomator.cryptolib.version>
<cryptomator.cryptofs.version>2.6.1</cryptomator.cryptofs.version>
<cryptomator.integrations.version>1.2.0-beta4</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.2.0-beta1</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.2.0-beta2</cryptomator.integrations.mac.version>
<cryptomator.integrations.linux.version>1.2.0-beta1</cryptomator.integrations.linux.version>
<cryptomator.fuse.version>2.0.0-beta4</cryptomator.fuse.version>
<cryptomator.dokany.version>2.0.0-beta2</cryptomator.dokany.version>
<cryptomator.webdav.version>2.0.0-beta4</cryptomator.webdav.version>
<!-- 3rd party dependencies -->
<commons-lang3.version>3.12.0</commons-lang3.version>
@@ -58,12 +59,18 @@
<!-- build-time dependencies -->
<jetbrains.annotations.version>23.0.0</jetbrains.annotations.version>
<dependency-check.version>7.4.0</dependency-check.version>
<dependency-check.version>7.4.4</dependency-check.version>
<jacoco.version>0.8.8</jacoco.version>
</properties>
<dependencies>
<!-- Cryptomator Libs -->
<dependency>
<!-- needed due to https://github.com/cryptomator/cryptolib/issues/34-->
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
<version>${cryptomator.cryptolib.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>

View File

@@ -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 <T, U> ObservableValue<U> mapWithDefault(ObservableValue<T> observable, Function<? super T, ? extends U> mapper, U defaultValue) {
return Bindings.createObjectBinding(() -> {
if (observable.getValue() == null) {
return defaultValue;
} else {
return mapper.apply(observable.getValue());
}
}, observable);
}
}

View File

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

View File

@@ -0,0 +1,9 @@
package org.cryptomator.common.mount;
public class IllegalMountPointException extends IllegalArgumentException {
public IllegalMountPointException(String msg) {
super(msg);
}
}

View File

@@ -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<MountService> provideMountService(Settings settings, List<MountService> 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<ActualMountService> provideMountService(Settings settings, List<MountService> 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<MountService> 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<ActualMountService> provideMountService(ActualMountService service) {
return new SimpleObjectProperty<>(service);
}
}

View File

@@ -0,0 +1,8 @@
package org.cryptomator.common.mount;
public class MountPointNotExistsException extends IllegalMountPointException {
public MountPointNotExistsException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,8 @@
package org.cryptomator.common.mount;
public class MountPointNotSupportedException extends IllegalMountPointException {
public MountPointNotSupportedException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,8 @@
package org.cryptomator.common.mount;
public class MountPointPreparationException extends RuntimeException {
public MountPointPreparationException(Throwable cause) {
super(cause);
}
}

View File

@@ -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);
}
}

View File

@@ -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<ActualMountService> mountServiceObservable;
@Inject
public Mounter(Settings settings, Environment env, WindowsDriveLetters driveLetters, ObservableValue<ActualMountService> 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) {
}
}

View File

@@ -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;

View File

@@ -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<Settings> {
@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> {
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());

View File

@@ -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<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE);

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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> cryptoFileSystem;
private final VaultState state;
private final ObjectProperty<Exception> lastKnownException;
private final ObservableValue<MountService> mountService;
private final ObservableValue<String> 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> mountPoint;
private final WindowsDriveLetters windowsDriveLetters;
private final Mounter mounter;
private final BooleanProperty showingStats;
private AtomicReference<MountHandle> mountHandle = new AtomicReference<>(null);
private AtomicReference<Mounter.MountHandle> mountHandle = new AtomicReference<>(null);
@Inject
Vault(Environment env, Settings settings, VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, ObservableValue<MountService> mountService, VaultStats stats, WindowsDriveLetters windowsDriveLetters) {
this.env = env;
this.settings = settings;
Vault(VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> 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<String> 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) {
}
}

View File

@@ -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:

View File

@@ -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"), //

View File

@@ -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"), //

View File

@@ -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)

View File

@@ -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<Scene> authFlowScene;
private final Lazy<Scene> noKeychainScene;
private final CompletableFuture<JWEObject> result;
private final DeviceKey deviceKey;
@Inject
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, CompletableFuture<JWEObject> result, DeviceKey deviceKey, @Named("windowTitle") String windowTitle) {
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy<Scene> noKeychainScene, CompletableFuture<JWEObject> 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> scene) {
Platform.runLater(() -> {
window.setScene(authFlowScene.get());
window.setScene(scene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {

View File

@@ -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();
}
}

View File

@@ -53,7 +53,7 @@ public class LockForcedController implements FxController {
}
public boolean isForceSupported() {
return false;//vault.supportsForcedUnmount(); TODO
return vault.supportsForcedUnmount();
}
}

View File

@@ -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<Vault> 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<Vault> selectedVault, WrongFileAlertComponent.Builder wrongFileAlert) {
public MainWindowController(@MainWindow Stage window, ObjectProperty<Vault> 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<Path> 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();
}
}

View File

@@ -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> 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<Vault, VaultStatisticsComponent> vaultStats;
private final VaultStatisticsComponent.Builder vaultStatsBuilder;
private final ObservableValue<Mountpoint> mountPoint;
private final ObservableValue<Boolean> accessibleViaPath;
private final ObservableValue<Boolean> accessibleViaUri;
private final ObservableValue<String> mountUri;
private final ObservableValue<String> mountPoint;
private final BooleanProperty draggingOver = new SimpleBooleanProperty();
private final BooleanProperty ciphertextPathsCopied = new SimpleBooleanProperty();
//FXML
public Button dropZone;
@Inject
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
public VaultDetailUnlockedController(ObjectProperty<Vault> 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<Path> 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<Path> 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<Path> 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<Path> 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<Path> 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<Vault> vaultProperty() {
@@ -101,13 +238,19 @@ public class VaultDetailUnlockedController implements FxController {
return accessibleViaUri.getValue();
}
public ObservableValue<String> mountUriProperty() {
return mountUri;
public ObservableValue<String> 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();
}
}

View File

@@ -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<Vault> 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<Vault> vaultList;
public StackPane root;
@Inject
VaultListController(@MainWindow Stage mainWindow, ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue) {
VaultListController(@MainWindow Stage mainWindow, ObservableList<Vault> vaults, ObjectProperty<Vault> 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<Path> 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();
}
}

View File

@@ -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<MountService> selectedMountService;
private final ResourceBundle resourceBundle;
private final BooleanExpression loopbackPortSupported;
private final ObservableValue<Boolean> mountToDirSupported;
private final ObservableValue<Boolean> mountToDriveLetterSupported;
private final ObservableValue<Boolean> mountFlagsSupported;
private final ObservableValue<Boolean> readonlySupported;
private final List<MountService> mountProviders;
public ChoiceBox<MountService> volumeTypeChoiceBox;
public TextField loopbackPortField;
public Button loopbackPortApplyButton;
@Inject
VolumePreferencesController(Settings settings, List<MountService> mountProviders, ObservableValue<MountService> selectedMountService) {
VolumePreferencesController(Settings settings, List<MountService> 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<Boolean> readonlySupportedProperty() {
return readonlySupported;
}
public boolean isReadonlySupported() {
return readonlySupported.getValue();
}
public ObservableValue<Boolean> mountToDirSupportedProperty() {
return mountToDirSupported;
}
public boolean isMountToDirSupported() {
return mountToDirSupported.getValue();
}
public ObservableValue<Boolean> mountToDriveLetterSupportedProperty() {
return mountToDriveLetterSupported;
}
public boolean isMountToDriveLetterSupported() {
return mountToDriveLetterSupported.getValue();
}
public ObservableValue<Boolean> mountFlagsSupportedProperty() {
return mountFlagsSupported;
}
public boolean isMountFlagsSupported() {
return mountFlagsSupported.getValue();
}
/* Helpers */
private static class MountServiceConverter extends StringConverter<MountService> {
private class MountServiceConverter extends StringConverter<MountService> {
@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();
}
}
}

View File

@@ -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<TrayMenuItem> 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();
}
});

View File

@@ -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);
}
}
}

View File

@@ -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<Throwable> 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<Throwable> 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();
}
}

View File

@@ -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<Throwable> unlockException() {
return new AtomicReference<>();
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_SUCCESS)
@UnlockScoped

View File

@@ -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<Boolean> {
private final Lazy<Scene> invalidMountPointScene;
private final FxApplicationWindows appWindows;
private final KeyLoadingStrategy keyLoadingStrategy;
private final AtomicReference<Throwable> unlockFailedException;
@Inject
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy) {
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, @UnlockWindow AtomicReference<Throwable> unlockFailedException) {
this.window = window;
this.vault = vault;
this.vaultService = vaultService;
@@ -48,6 +51,7 @@ public class UnlockWorkflow extends Task<Boolean> {
this.invalidMountPointScene = invalidMountPointScene;
this.appWindows = appWindows;
this.keyLoadingStrategy = keyLoadingStrategy;
this.unlockFailedException = unlockFailedException;
}
@Override
@@ -67,13 +71,15 @@ public class UnlockWorkflow extends Task<Boolean> {
} 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<Boolean> {
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);
}

View File

@@ -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<String> defaultMountFlags;
private final ObservableValue<Boolean> mountpointDirSupported;
private final ObservableValue<Boolean> mountpointDriveLetterSupported;
private final ObservableValue<Boolean> readOnlySupported;
private final ObservableValue<Boolean> mountFlagsSupported;
private final ObservableValue<Path> driveLetter;
private final ObservableValue<String> directoryPath;
private final FxApplicationWindows applicationWindows;
//-- FXML objects --
@@ -54,59 +58,75 @@ public class MountOptionsController implements FxController {
public ChoiceBox<Path> driveLetterSelection;
@Inject
MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue<MountService> mountService, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, Environment environment) {
MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue<ActualMountService> 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<? extends Toggle> 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<Boolean> readOnlySupportedProperty() {
return mountpointDriveLetterSupported;
return readOnlySupported;
}
public boolean isReadOnlySupported() {
return readOnlySupported.getValue();
}
public ObservableValue<Path> driveLetterProperty() {
return driveLetter;
}
public Path getDriveLetter() {
return driveLetter.getValue();
}
public ObservableValue<String> directoryPathProperty() {
return directoryPath;
}
@@ -259,5 +274,4 @@ public class MountOptionsController implements FxController {
public String getDirectoryPath() {
return directoryPath.getValue();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.hub.NoKeychainController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_LEFT">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="EXCLAMATION" glyphSize="24"/>
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS">
<Label styleClass="label-large" text="%hub.noKeychain.message" wrapText="true" textAlignment="LEFT">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<FormattedLabel format="%hub.noKeychain.description" arg1="%preferences.general.keychainBackend" wrapText="true" textAlignment="LEFT"/>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="false" cancelButton="true" onAction="#cancel"/>
<Button text="%hub.noKeychain.openBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#openPreferences"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</HBox>

View File

@@ -1,10 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<StackPane xmlns:fx="http://javafx.com/fxml"
@@ -14,24 +10,10 @@
styleClass="main-window">
<VBox minWidth="650">
<fx:include source="main_window_title.fxml" VBox.vgrow="NEVER"/>
<StackPane VBox.vgrow="ALWAYS">
<SplitPane dividerPositions="0.33" orientation="HORIZONTAL">
<fx:include source="vault_list.fxml" SplitPane.resizableWithParent="false"/>
<fx:include source="vault_detail.fxml" SplitPane.resizableWithParent="true"/>
</SplitPane>
<VBox styleClass="drag-n-drop-indicator" visible="${controller.draggingOver}" alignment="TOP_CENTER">
<HBox visible="${!controller.draggingVaultOver}" managed="${!controller.draggingVaultOver}" spacing="6" styleClass="drag-n-drop-header" alignment="CENTER" VBox.vgrow="NEVER">
<FontAwesome5IconView glyph="EXCLAMATION_TRIANGLE"/>
<Label text="%main.dropZone.unknownDragboardContent"/>
</HBox>
<HBox visible="${controller.draggingVaultOver}" managed="${controller.draggingVaultOver}" spacing="6" styleClass="drag-n-drop-header" alignment="CENTER" VBox.vgrow="NEVER">
<FontAwesome5IconView glyph="CHECK"/>
<Label text="%main.dropZone.dropVault"/>
</HBox>
<Region VBox.vgrow="ALWAYS"/>
</VBox>
</StackPane>
<SplitPane dividerPositions="0.33" orientation="HORIZONTAL" VBox.vgrow="ALWAYS">
<fx:include source="vault_list.fxml" SplitPane.resizableWithParent="false"/>
<fx:include source="vault_detail.fxml" SplitPane.resizableWithParent="true"/>
</SplitPane>
</VBox>
<fx:include source="main_window_resize.fxml"/>
</StackPane>

View File

@@ -6,16 +6,20 @@
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.migration.MigrationStartController"
minWidth="400"
maxWidth="400"
minHeight="145"
prefWidth="580"
prefHeight="350"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
@@ -23,13 +27,47 @@
<children>
<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="FILE_IMPORT" glyphSize="24"/>
<padding>
<Insets left="12"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="48"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="FILE_IMPORT" glyphSize="48"/>
</StackPane>
<VBox spacing="6" HBox.hgrow="ALWAYS">
<FormattedLabel format="%migration.start.prompt" arg1="${controller.vault.displayName}" wrapText="true"/>
<CheckBox fx:id="confirmSyncDone" text="%migration.start.confirm"/>
<VBox HBox.hgrow="ALWAYS" alignment="CENTER">
<padding>
<Insets top="0" right="12" bottom="0" left="12"/>
</padding>
<Label text="%migration.start.header" styleClass="label-extra-large"/>
<Region minHeight="15"/>
<VBox>
<FormattedLabel format="%migration.start.text" arg1="${controller.vault.displayName}" wrapText="true"/>
<GridPane alignment="CENTER_LEFT" >
<padding>
<Insets left="6"/>
</padding>
<columnConstraints>
<ColumnConstraints minWidth="20" halignment="LEFT"/>
<ColumnConstraints fillWidth="true"/>
</columnConstraints>
<rowConstraints>
<RowConstraints valignment="TOP"/>
<RowConstraints valignment="TOP"/>
<RowConstraints valignment="TOP"/>
<RowConstraints valignment="TOP"/>
</rowConstraints>
<Label text="1." GridPane.rowIndex="0" GridPane.columnIndex="0" />
<Label text="%migration.start.remarkUndone" wrapText="true" GridPane.rowIndex="0" GridPane.columnIndex="1" />
<Label text="2." GridPane.rowIndex="1" GridPane.columnIndex="0" />
<Label text="%migration.start.remarkVersions" wrapText="true" GridPane.rowIndex="1" GridPane.columnIndex="1" />
<Label text="3." GridPane.rowIndex="2" GridPane.columnIndex="0" />
<Label text="%migration.start.remarkCanRun" wrapText="true" GridPane.rowIndex="2" GridPane.columnIndex="1" />
<Label text="4." GridPane.rowIndex="3" GridPane.columnIndex="0" />
<Label text="%migration.start.remarkSynced" wrapText="true" GridPane.rowIndex="3" GridPane.columnIndex="1" />
</GridPane>
<Region minHeight="15"/>
<CheckBox fx:id="confirmSyncDone" text="%migration.start.confirm"/>
</VBox>
</VBox>
</HBox>

View File

@@ -22,7 +22,7 @@
</ImageView>
<VBox spacing="3" HBox.hgrow="ALWAYS" alignment="CENTER_LEFT">
<FormattedLabel styleClass="label-extra-large" format="Cryptomator %s" arg1="${controller.fullApplicationVersion}"/>
<Label text="© 2016 2022 Skymatic GmbH"/>
<Label text="© 2016 2023 Skymatic GmbH"/>
</VBox>
</HBox>

View File

@@ -1,10 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.NumericTextField?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ChoiceBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
@@ -20,10 +22,41 @@
<ChoiceBox fx:id="volumeTypeChoiceBox"/>
</HBox>
<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.loopbackPortSupported}">
<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.loopbackPortSupported}" managed="${controller.loopbackPortSupported}">
<Label text="%preferences.volume.tcp.port"/>
<NumericTextField fx:id="loopbackPortField"/>
<Button text="%generic.button.apply" fx:id="loopbackPortApplyButton" onAction="#doChangeLoopbackPort"/>
</HBox>
<Separator orientation="HORIZONTAL"/>
<Label text="%preferences.volume.supportedFeatures"/>
<VBox spacing="12">
<Label text="%preferences.volume.feature.mountAuto">
<graphic>
<FontAwesome5IconView glyph="CHECK"/>
</graphic>
</Label>
<Label text="%preferences.volume.feature.mountToDir" visible="${controller.mountToDirSupported}" managed="${controller.mountToDirSupported}" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="CHECK"/>
</graphic>
</Label>
<Label text="%preferences.volume.feature.mountToDriveLetter" visible="${controller.mountToDriveLetterSupported}" managed="${controller.mountToDriveLetterSupported}">
<graphic>
<FontAwesome5IconView glyph="CHECK"/>
</graphic>
</Label>
<Label text="%preferences.volume.feature.mountFlags" visible="${controller.mountFlagsSupported}" managed="${controller.mountFlagsSupported}">
<graphic>
<FontAwesome5IconView glyph="CHECK"/>
</graphic>
</Label>
<Label text="%preferences.volume.feature.readOnly" visible="${controller.readonlySupported}" managed="${controller.readonlySupported}">
<graphic>
<FontAwesome5IconView glyph="CHECK"/>
</graphic>
</Label>
</VBox>
</children>
</VBox>

View File

@@ -34,16 +34,19 @@
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS">
<Label styleClass="label-large" text="%unlock.error.message" wrapText="true" textAlignment="LEFT">
<Label styleClass="label-large" text="%unlock.error.customPath.message" wrapText="true" textAlignment="LEFT">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<FormattedLabel fx:id="dialogDescription" wrapText="true" textAlignment="LEFT"/>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button text="%hub.noKeychain.openBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#closeAndOpenPreferences"/>
</buttons>
</ButtonBar>
</VBox>

View File

@@ -2,10 +2,11 @@
<?import org.cryptomator.ui.controls.ThroughputLabel?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.geometry.Insets?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.mainwindow.VaultDetailUnlockedController"
@@ -18,6 +19,7 @@
<FontAwesome5IconView glyph="HDD" glyphSize="24"/>
<VBox spacing="4" alignment="CENTER_LEFT">
<Label text="%main.vaultDetail.revealBtn"/>
<Label styleClass="label-extra-small" text="${controller.mountPoint}" textOverrun="CENTER_ELLIPSIS"/>
</VBox>
</HBox>
</graphic>
@@ -27,8 +29,8 @@
<HBox spacing="12" alignment="CENTER">
<FontAwesome5IconView glyph="LINK" glyphSize="24"/>
<VBox spacing="4" alignment="CENTER_LEFT">
<Label text="%generic.button.copy"/> <!-- TODO -->
<Label styleClass="label-extra-small" text="${controller.mountUri}" textOverrun="CENTER_ELLIPSIS"/>
<Label text="%main.vaultDetail.copyUri"/>
<Label styleClass="label-extra-small" text="${controller.mountPoint}" textOverrun="CENTER_ELLIPSIS"/>
</VBox>
</HBox>
</graphic>
@@ -41,7 +43,28 @@
<Region VBox.vgrow="ALWAYS"/>
<HBox alignment="BOTTOM_RIGHT">
<HBox alignment="BOTTOM_CENTER">
<HBox visible="${controller.accessibleViaPath}" managed="${controller.accessibleViaPath}">
<padding>
<Insets topRightBottomLeft="0"/>
</padding>
<Button fx:id="dropZone" styleClass="drag-n-drop" text="%main.vaultDetail.locateEncryptedFileBtn" minWidth="120" maxWidth="180" wrapText="true" textAlignment="CENTER" onAction="#chooseFileAndReveal" contentDisplay="TOP" visible="${!controller.ciphertextPathsCopied}" managed="${!controller.ciphertextPathsCopied}">
<graphic>
<FontAwesome5IconView glyph="FILE_DOWNLOAD" glyphSize="15"/>
</graphic>
<tooltip>
<Tooltip text="%main.vaultDetail.locateEncryptedFileBtn.tooltip"/>
</tooltip>
</Button>
<Button styleClass="drag-n-drop" text="%main.vaultDetail.encryptedPathsCopied" minWidth="120" maxWidth="180" wrapText="true" textAlignment="CENTER" onAction="#chooseFileAndReveal" contentDisplay="TOP" visible="${controller.ciphertextPathsCopied}" managed="${controller.ciphertextPathsCopied}">
<graphic>
<FontAwesome5IconView glyph="CHECK" glyphSize="15"/>
</graphic>
</Button>
</HBox>
<Region HBox.hgrow="ALWAYS"/>
<Button text="%main.vaultDetail.stats" minWidth="120" onAction="#showVaultStatistics" contentDisplay="BOTTOM">
<graphic>
<VBox spacing="6">

View File

@@ -8,25 +8,29 @@
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Arc?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.mainwindow.VaultListController"
minWidth="206">
<StackPane VBox.vgrow="ALWAYS">
<ListView fx:id="vaultList" editable="true" fixedCellSize="60">
<contextMenu>
<fx:include source="vault_list_contextmenu.fxml"/>
</contextMenu>
</ListView>
<VBox visible="${controller.emptyVaultList}" spacing="6" alignment="CENTER">
<Region VBox.vgrow="ALWAYS"/>
<Label VBox.vgrow="NEVER" text="%main.vaultlist.emptyList.onboardingInstruction" textAlignment="CENTER" wrapText="true"/>
<Arc VBox.vgrow="NEVER" styleClass="onboarding-overlay-arc" type="OPEN" centerX="50" centerY="0" radiusY="100" radiusX="50" startAngle="0" length="-60" strokeWidth="1"/>
</VBox>
</StackPane>
<Button styleClass="toolbar-button" text="%main.vaultlist.addVaultBtn" onAction="#didClickAddVault" alignment="BASELINE_CENTER" maxWidth="Infinity">
<graphic>
<FontAwesome5IconView glyph="PLUS"/>
</graphic>
</Button>
</VBox>
<StackPane xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:id="root"
fx:controller="org.cryptomator.ui.mainwindow.VaultListController"
minWidth="206">
<VBox>
<StackPane VBox.vgrow="ALWAYS">
<ListView fx:id="vaultList" editable="true" fixedCellSize="60">
<contextMenu>
<fx:include source="vault_list_contextmenu.fxml"/>
</contextMenu>
</ListView>
<VBox visible="${controller.emptyVaultList}" spacing="6" alignment="CENTER">
<Region VBox.vgrow="ALWAYS"/>
<Label VBox.vgrow="NEVER" text="%main.vaultlist.emptyList.onboardingInstruction" textAlignment="CENTER" wrapText="true"/>
<Arc VBox.vgrow="NEVER" styleClass="onboarding-overlay-arc" type="OPEN" centerX="50" centerY="0" radiusY="100" radiusX="50" startAngle="0" length="-60" strokeWidth="1"/>
</VBox>
</StackPane>
<Button styleClass="toolbar-button" text="%main.vaultlist.addVaultBtn" onAction="#didClickAddVault" alignment="BASELINE_CENTER" maxWidth="Infinity">
<graphic>
<FontAwesome5IconView glyph="PLUS"/>
</graphic>
</Button>
</VBox>
<Region styleClass="drag-n-drop-border" visible="${controller.draggingVaultOver}"/>
</StackPane>

View File

@@ -5,12 +5,15 @@
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.ChoiceBox?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.text.TextFlow?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.vaultoptions.MountOptionsController"
@@ -22,7 +25,11 @@
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Label text="Options depend on the selected volume provider in the general preferences"/>
<TextFlow>
<Text text="%vaultOptions.mount.info"/>
<Text text=" "/>
<Hyperlink styleClass="hyperlink-underline" text="%vaultOptions.mount.linkToPreferences" onAction="#openVolumePreferences" wrapText="true"/>
</TextFlow>
<CheckBox fx:id="readOnlyCheckbox" text="%vaultOptions.mount.readonly" visible="${controller.readOnlySupported}" managed="${controller.readOnlySupported}"/>
<VBox visible="${controller.mountFlagsSupported}" managed="${controller.mountFlagsSupported}">
@@ -44,22 +51,24 @@
<HBox spacing="6" visible="${controller.mountpointDriveLetterSupported}" managed="${controller.mountpointDriveLetterSupported}">
<RadioButton toggleGroup="${mountPointToggleGroup}" fx:id="mountPointDriveLetterBtn" text="%vaultOptions.mount.mountPoint.driveLetter"/>
<ChoiceBox fx:id="driveLetterSelection" disable="${!mountPointDriveLetterBtn.selected}" value="${controller.driveLetter}"/>
<ChoiceBox fx:id="driveLetterSelection" disable="${!mountPointDriveLetterBtn.selected}"/>
</HBox>
<HBox spacing="6" alignment="CENTER_LEFT" visible="${controller.mountpointDirSupported}" managed="${controller.mountpointDirSupported}">
<RadioButton toggleGroup="${mountPointToggleGroup}" fx:id="mountPointDirBtn" text="%vaultOptions.mount.mountPoint.custom"/>
<Button text="%vaultOptions.mount.mountPoint.directoryPickerButton" onAction="#chooseCustomMountPoint" contentDisplay="LEFT" disable="${!mountPointDirBtn.selected}">
<graphic>
<FontAwesome5IconView glyph="FOLDER_OPEN" glyphSize="15"/>
</graphic>
</Button>
</HBox>
<TextField fx:id="directoryPathField" text="${controller.directoryPath}" visible="${mountPointDirBtn.selected}" managed="${mountPointDirBtn.managed}" maxWidth="Infinity" editable="false" >
<VBox.margin>
<Insets left="24"/>
</VBox.margin>
</TextField>
<VBox spacing="6" visible="${controller.mountpointDirSupported}" managed="${controller.mountpointDirSupported}">
<HBox spacing="6" alignment="CENTER_LEFT">
<RadioButton toggleGroup="${mountPointToggleGroup}" fx:id="mountPointDirBtn" text="%vaultOptions.mount.mountPoint.custom"/>
<Button text="%vaultOptions.mount.mountPoint.directoryPickerButton" onAction="#chooseCustomMountPoint" contentDisplay="LEFT" disable="${!mountPointDirBtn.selected}">
<graphic>
<FontAwesome5IconView glyph="FOLDER_OPEN" glyphSize="15"/>
</graphic>
</Button>
</HBox>
<TextField fx:id="directoryPathField" text="${controller.directoryPath}" visible="${mountPointDirBtn.selected}" managed="${mountPointDirBtn.managed}" maxWidth="Infinity" editable="false">
<VBox.margin>
<Insets left="24"/>
</VBox.margin>
</TextField>
</VBox>
</children>
</VBox>

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 B

View File

@@ -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"));
}
*/
}