diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
index dfabee3e5..4775a0aa2 100644
--- a/.github/ISSUE_TEMPLATE/bug.md
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -5,6 +5,7 @@ labels: type:bug
---
+
### Description
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c38d20dc5..d7b93c2e3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,6 +7,7 @@ jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
+ if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
@@ -27,24 +28,27 @@ jobs:
run: |
curl -o ~/codacy-coverage-reporter.jar https://repo.maven.apache.org/maven2/com/codacy/codacy-coverage-reporter/7.1.0/codacy-coverage-reporter-7.1.0-assembly.jar
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/commons/target/site/jacoco/jacoco.xml --partial
- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/keychain/target/site/jacoco/jacoco.xml --partial
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/ui/target/site/jacoco/jacoco.xml --partial
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/launcher/target/site/jacoco/jacoco.xml --partial
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar final
env:
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
- - name: Assemble Buildkit
- run: mvn -B package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease
+ - name: Assemble buildkit-linux.zip
+ run: mvn -B clean package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease,linux
- name: Upload buildkit-linux.zip
uses: actions/upload-artifact@v1
with:
name: buildkit-linux.zip
path: main/buildkit/target/buildkit-linux.zip
+ - name: Assemble buildkit-mac.zip
+ run: mvn -B clean package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease,mac
- name: Upload buildkit-mac.zip
uses: actions/upload-artifact@v1
with:
name: buildkit-mac.zip
path: main/buildkit/target/buildkit-mac.zip
+ - name: Assemble buildkit-win.zip
+ run: mvn -B clean package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease,windows
- name: Upload buildkit-win.zip
uses: actions/upload-artifact@v1
with:
diff --git a/.github/workflows/triageBugs.yml b/.github/workflows/triageBugs.yml
new file mode 100644
index 000000000..f3354fb11
--- /dev/null
+++ b/.github/workflows/triageBugs.yml
@@ -0,0 +1,25 @@
+name: Bug Report Triage
+
+on:
+ issues:
+ types: [opened]
+
+jobs:
+ closeTemplateViolation:
+ name: Close bug reports that violate the issue template
+ runs-on: ubuntu-latest
+ steps:
+ - if: |
+ contains(github.event.issue.labels.*.name, 'type:bug')
+ && (
+ !contains(github.event.issue.body, '')
+ || !contains(github.event.issue.body, '### Description')
+ )
+ name: Close Issue
+ uses: peter-evans/close-issue@v1
+ with:
+ comment: |
+ This bug report did ignore our issue template. 😞
+ Auto-closing this issue, since it is most likely not useful.
+
+ _This decision was made by a bot. If you think the bot is wrong, let us know and we'll reopen this issue._
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3f055e749..900a21ae0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,5 +18,8 @@ pom.xml.versionsBackup
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
+.idea/compiler.xml
+.idea/encodings.xml
+.idea/jarRepositories.xml
.idea/**/libraries/
*.iml
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index d6cb2c866..d361191e8 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -9,14 +9,36 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -30,12 +52,17 @@
+
+
+
+
+
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index e8dfd8106..000000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
deleted file mode 100644
index c3807468d..000000000
--- a/.idea/encodings.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
deleted file mode 100644
index 727c7b1bb..000000000
--- a/.idea/jarRepositories.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Cryptomator_Linux_Dev.xml b/.idea/runConfigurations/Cryptomator_Linux_Dev.xml
new file mode 100644
index 000000000..945c99d68
--- /dev/null
+++ b/.idea/runConfigurations/Cryptomator_Linux_Dev.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Cryptomator_Windows.xml b/.idea/runConfigurations/Cryptomator_Windows.xml
index 05852ad59..c1143a530 100644
--- a/.idea/runConfigurations/Cryptomator_Windows.xml
+++ b/.idea/runConfigurations/Cryptomator_Windows.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/.idea/runConfigurations/Cryptomator_Windows_Dev.xml b/.idea/runConfigurations/Cryptomator_Windows_Dev.xml
new file mode 100644
index 000000000..1cea10200
--- /dev/null
+++ b/.idea/runConfigurations/Cryptomator_Windows_Dev.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Cryptomator_macOS_Dev.xml b/.idea/runConfigurations/Cryptomator_macOS_Dev.xml
new file mode 100644
index 000000000..bda578f65
--- /dev/null
+++ b/.idea/runConfigurations/Cryptomator_macOS_Dev.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 091b6af8c..e07ab285f 100644
--- a/README.md
+++ b/README.md
@@ -73,7 +73,9 @@ For more information on the security details visit [cryptomator.org](https://doc
```
cd main
-mvn clean install -Prelease
+mvn clean install -Prelease,windows
+# or mvn clean install -Prelease,mac
+# or mvn clean install -Prelease,linux
```
This will build all the jars and bundle them together with their OS-specific dependencies under `main/buildkit/target`. This can now be used to build native packages.
diff --git a/main/buildkit/assembly-linux.xml b/main/buildkit/assembly-linux.xml
index b618c7d15..1e16da9e2 100644
--- a/main/buildkit/assembly-linux.xml
+++ b/main/buildkit/assembly-linux.xml
@@ -1,5 +1,5 @@
-tarballfalse
@@ -14,13 +14,6 @@
libs
-
- target/
-
- ffi-version.txt
-
- libs
- target/
@@ -43,12 +36,5 @@
libs
-
- target/linux-libs
-
- *.jar
-
- libs
-
\ No newline at end of file
diff --git a/main/buildkit/assembly-mac.xml b/main/buildkit/assembly-mac.xml
index 3b1bf9666..4a01f4067 100644
--- a/main/buildkit/assembly-mac.xml
+++ b/main/buildkit/assembly-mac.xml
@@ -1,5 +1,5 @@
-tarballfalse
@@ -14,13 +14,6 @@
libs
-
- target/
-
- ffi-version.txt
-
- libs
- target/
@@ -43,12 +36,5 @@
libs
-
- target/mac-libs
-
- *.jar
-
- libs
-
\ No newline at end of file
diff --git a/main/buildkit/assembly-win.xml b/main/buildkit/assembly-win.xml
index 928994c5f..0297f3ec4 100644
--- a/main/buildkit/assembly-win.xml
+++ b/main/buildkit/assembly-win.xml
@@ -1,5 +1,5 @@
-tarballfalse
@@ -14,13 +14,6 @@
libs
-
- target/
-
- ffi-version.txt
-
- libs
- target/
@@ -43,12 +36,5 @@
libs
-
- target/win-libs
-
- *.jar
-
- libs
-
\ No newline at end of file
diff --git a/main/buildkit/pom.xml b/main/buildkit/pom.xml
index db648f3bd..305b35390 100644
--- a/main/buildkit/pom.xml
+++ b/main/buildkit/pom.xml
@@ -1,10 +1,10 @@
-
+4.0.0org.cryptomatormain
- 1.5.8
+ 1.5.9buildkitpom
@@ -24,7 +24,6 @@
org.apache.maven.pluginsmaven-resources-plugin
- 3.1.0copy-resources
@@ -55,8 +54,8 @@
+ org.apache.maven.pluginsmaven-dependency-plugin
- 3.1.1copy-libs
@@ -65,110 +64,153 @@
copy-dependencies
+ runtime${project.build.directory}/libslinux,mac,win
- dbus-java,secret-service,hkdf,java-utils
-
-
-
- copy-linux-libs
- prepare-package
-
- copy-dependencies
-
-
- ${project.build.directory}/linux-libs
- org.openjfx
- linux
-
-
-
- copy-linux-secret-service
- prepare-package
-
- copy-dependencies
-
-
- ${project.build.directory}/linux-libs
- dbus-java,secret-service,hkdf,java-utils
-
-
-
- copy-mac-libs
- prepare-package
-
- copy-dependencies
-
-
- ${project.build.directory}/mac-libs
- org.openjfx
- mac
-
-
-
- copy-win-libs
- prepare-package
-
- copy-dependencies
-
-
- ${project.build.directory}/win-libs
- org.openjfx
- win
-
-
-
-
-
-
-
- maven-assembly-plugin
- 3.1.1
-
-
- assemble-linux
- package
-
- single
-
-
-
- assembly-linux.xml
-
- false
- buildkit-linux
-
-
-
- assemble-mac
- package
-
- single
-
-
-
- assembly-mac.xml
-
- false
- buildkit-mac
-
-
-
- assemble-win
- package
-
- single
-
-
-
- assembly-win.xml
-
- false
- buildkit-win
+
+
+
+ linux
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ assemble-linux
+ package
+
+ single
+
+
+
+ assembly-linux.xml
+
+ false
+ buildkit-linux
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ copy-linux-libs
+ prepare-package
+
+ copy-dependencies
+
+
+ ${project.build.directory}/libs
+ org.openjfx
+ linux
+
+
+
+
+
+
+
+
+
+ mac
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ assemble-mac
+ package
+
+ single
+
+
+
+ assembly-mac.xml
+
+ false
+ buildkit-mac
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ copy-mac-libs
+ prepare-package
+
+ copy-dependencies
+
+
+ ${project.build.directory}/libs
+ org.openjfx
+ mac
+
+
+
+
+
+
+
+
+
+ windows
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ assemble-win
+ package
+
+ single
+
+
+
+ assembly-win.xml
+
+ false
+ buildkit-win
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ copy-win-libs
+ prepare-package
+
+ copy-dependencies
+
+
+ ${project.build.directory}/libs
+ org.openjfx
+ win
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main/buildkit/src/main/resources/ffi-version.txt b/main/buildkit/src/main/resources/ffi-version.txt
deleted file mode 100644
index 1853c270c..000000000
--- a/main/buildkit/src/main/resources/ffi-version.txt
+++ /dev/null
@@ -1 +0,0 @@
-${cryptomator.jni.version}
\ No newline at end of file
diff --git a/main/buildkit/src/main/resources/launcher-linux.sh b/main/buildkit/src/main/resources/launcher-linux.sh
index 2322870e8..6ceb933b0 100644
--- a/main/buildkit/src/main/resources/launcher-linux.sh
+++ b/main/buildkit/src/main/resources/launcher-linux.sh
@@ -1,4 +1,5 @@
#!/bin/sh
+cd $(dirname $0)
java \
-cp "libs/*" \
-Dcryptomator.settingsPath="~/.config/Cryptomator/settings.json" \
@@ -6,6 +7,6 @@ java \
-Dcryptomator.logDir="~/.local/share/Cryptomator/logs" \
-Dcryptomator.mountPointsDir="~/.local/share/Cryptomator/mnt" \
-Djdk.gtk.version=2 \
- -Xss20m \
+ -Xss2m \
-Xmx512m \
- org.cryptomator.launcher.Cryptomator
\ No newline at end of file
+ org.cryptomator.launcher.Cryptomator
diff --git a/main/buildkit/src/main/resources/launcher-mac.sh b/main/buildkit/src/main/resources/launcher-mac.sh
index 65f8ba10d..2c06efab2 100644
--- a/main/buildkit/src/main/resources/launcher-mac.sh
+++ b/main/buildkit/src/main/resources/launcher-mac.sh
@@ -1,4 +1,5 @@
#!/bin/sh
+cd $(dirname $0)
java \
-cp "libs/*" \
-Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" \
diff --git a/main/buildkit/src/main/resources/launcher-win.bat b/main/buildkit/src/main/resources/launcher-win.bat
index 505981c9e..d385d7e28 100644
--- a/main/buildkit/src/main/resources/launcher-win.bat
+++ b/main/buildkit/src/main/resources/launcher-win.bat
@@ -4,6 +4,7 @@ java ^
-Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" ^
-Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator/ipcPort.bin" ^
-Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" ^
+ -Dcryptomator.mountPointsDir="~/Cryptomator" ^
-Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" ^
-Xss20m ^
-Xmx512m ^
diff --git a/main/commons/pom.xml b/main/commons/pom.xml
index 7ff7a22bb..c5806b0cd 100644
--- a/main/commons/pom.xml
+++ b/main/commons/pom.xml
@@ -1,10 +1,10 @@
-
+4.0.0org.cryptomatormain
- 1.5.8
+ 1.5.9commonsCryptomator Commons
@@ -29,7 +29,7 @@
org.cryptomator
- jni
+ integrations-api
@@ -44,7 +44,7 @@
- org.fxmisc.easybind
+ com.tobiasdiezeasybind
diff --git a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java
index bac0114da..ed278e1ab 100644
--- a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java
+++ b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java
@@ -5,24 +5,25 @@
*******************************************************************************/
package org.cryptomator.common;
+import com.tobiasdiez.easybind.EasyBind;
import dagger.Module;
import dagger.Provides;
-import javafx.beans.binding.Binding;
-import javafx.beans.binding.Bindings;
-import javafx.collections.ObservableList;
import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.keychain.KeychainModule;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultComponent;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.frontend.webdav.WebDavServer;
-import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Singleton;
+import javafx.beans.binding.Binding;
+import javafx.beans.binding.Bindings;
+import javafx.collections.ObservableList;
import java.net.InetSocketAddress;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
@@ -33,7 +34,7 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
-@Module(subcomponents = {VaultComponent.class})
+@Module(subcomponents = {VaultComponent.class}, includes = {KeychainModule.class})
public abstract class CommonsModule {
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
diff --git a/main/commons/src/main/java/org/cryptomator/common/Environment.java b/main/commons/src/main/java/org/cryptomator/common/Environment.java
index 890840b7d..d920b1b9f 100644
--- a/main/commons/src/main/java/org/cryptomator/common/Environment.java
+++ b/main/commons/src/main/java/org/cryptomator/common/Environment.java
@@ -40,6 +40,7 @@ public class Environment {
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
LOG.debug("cryptomator.buildNumber: {}", System.getProperty("cryptomator.buildNumber"));
+ LOG.debug("fuse.experimental: {}", Boolean.getBoolean("fuse.experimental"));
}
public boolean useCustomLogbackConfig() {
@@ -74,6 +75,10 @@ public class Environment {
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
}
+ public boolean useExperimentalFuse() {
+ return Boolean.getBoolean("fuse.experimental");
+ }
+
private int getInt(String propertyName, int defaultValue) {
String value = System.getProperty(propertyName);
try {
diff --git a/main/commons/src/main/java/org/cryptomator/common/JniModule.java b/main/commons/src/main/java/org/cryptomator/common/JniModule.java
deleted file mode 100644
index 180736710..000000000
--- a/main/commons/src/main/java/org/cryptomator/common/JniModule.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2019 Skymatic GmbH.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.common;
-
-import dagger.Module;
-import dagger.Provides;
-import org.cryptomator.jni.JniFunctions;
-import org.cryptomator.jni.MacFunctions;
-import org.cryptomator.jni.WinFunctions;
-
-import javax.inject.Singleton;
-import java.util.Optional;
-
-@Module
-public class JniModule {
-
- @Provides
- @Singleton
- Optional provideOptionalMacFunctions() {
- return JniFunctions.macFunctions();
- }
-
- @Provides
- @Singleton
- Optional provideOptionalWinFunctions() {
- return JniFunctions.winFunctions();
- }
-
-}
diff --git a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java
index d58d5af36..7bca69457 100644
--- a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java
+++ b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java
@@ -5,12 +5,12 @@
*******************************************************************************/
package org.cryptomator.common;
+import com.google.common.base.Throwables;
+
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
-import com.google.common.base.Throwables;
-
public final class LazyInitializer {
private LazyInitializer() {
@@ -18,7 +18,7 @@ public final class LazyInitializer {
/**
* Same as {@link #initializeLazily(AtomicReference, SupplierThrowingException, Class)} except that no checked exception may be thrown by the factory function.
- *
+ *
* @param Type of the value
* @param reference A reference to a maybe not yet initialized value.
* @param factory A factory providing a value for the reference, if it doesn't exist yet. The factory may be invoked multiple times, but only one result will survive.
@@ -31,7 +31,7 @@ public final class LazyInitializer {
/**
* Threadsafe lazy initialization pattern as proposed on http://stackoverflow.com/a/30247202/4014509
- *
+ *
* @param Type of the value
* @param Type of the any expected exception that may occur during initialization
* @param reference A reference to a maybe not yet initialized value.
diff --git a/main/commons/src/main/java/org/cryptomator/common/LicenseHolder.java b/main/commons/src/main/java/org/cryptomator/common/LicenseHolder.java
index 70d439afe..a31807a8d 100644
--- a/main/commons/src/main/java/org/cryptomator/common/LicenseHolder.java
+++ b/main/commons/src/main/java/org/cryptomator/common/LicenseHolder.java
@@ -1,15 +1,15 @@
package org.cryptomator.common;
import com.auth0.jwt.interfaces.DecodedJWT;
+import org.cryptomator.common.settings.Settings;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
-import org.cryptomator.common.settings.Settings;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
import java.util.Optional;
@Singleton
diff --git a/main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java b/main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java
index 96588e6b5..e0ff9ae25 100644
--- a/main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java
+++ b/main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java
@@ -8,10 +8,10 @@
*******************************************************************************/
package org.cryptomator.common;
-import java.util.Comparator;
-
import org.apache.commons.lang3.StringUtils;
+import java.util.Comparator;
+
/**
* Compares version strings according to SemVer 2.0.0.
*/
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java b/main/commons/src/main/java/org/cryptomator/common/keychain/KeychainManager.java
similarity index 70%
rename from main/keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java
rename to main/commons/src/main/java/org/cryptomator/common/keychain/KeychainManager.java
index 300b5b035..537b83577 100644
--- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java
+++ b/main/commons/src/main/java/org/cryptomator/common/keychain/KeychainManager.java
@@ -1,60 +1,71 @@
-package org.cryptomator.keychain;
+package org.cryptomator.common.keychain;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
+import org.cryptomator.integrations.keychain.KeychainAccessException;
+import org.cryptomator.integrations.keychain.KeychainAccessProvider;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
import javafx.application.Platform;
+import javafx.beans.binding.ObjectExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import java.util.Arrays;
-public class KeychainManager implements KeychainAccessStrategy {
+@Singleton
+public class KeychainManager implements KeychainAccessProvider {
- private static final Logger LOG = LoggerFactory.getLogger(KeychainManager.class);
+ private final ObjectExpression keychain;
+ private final LoadingCache passphraseStoredProperties;
- private final KeychainAccessStrategy keychain;
- private LoadingCache passphraseStoredProperties;
-
- KeychainManager(KeychainAccessStrategy keychain) {
- assert keychain.isSupported();
- this.keychain = keychain;
+ @Inject
+ KeychainManager(ObjectExpression selectedKeychain) {
+ this.keychain = selectedKeychain;
this.passphraseStoredProperties = CacheBuilder.newBuilder() //
.weakValues() //
.build(CacheLoader.from(this::createStoredPassphraseProperty));
+ keychain.addListener(ignored -> passphraseStoredProperties.invalidateAll());
+ }
+
+ private KeychainAccessProvider getKeychainOrFail() throws KeychainAccessException {
+ var result = keychain.getValue();
+ if (result == null) {
+ throw new NoKeychainAccessProviderException();
+ }
+ return result;
}
@Override
public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
- keychain.storePassphrase(key, passphrase);
+ getKeychainOrFail().storePassphrase(key, passphrase);
setPassphraseStored(key, true);
}
@Override
public char[] loadPassphrase(String key) throws KeychainAccessException {
- char[] passphrase = keychain.loadPassphrase(key);
+ char[] passphrase = getKeychainOrFail().loadPassphrase(key);
setPassphraseStored(key, passphrase != null);
return passphrase;
}
@Override
public void deletePassphrase(String key) throws KeychainAccessException {
- keychain.deletePassphrase(key);
+ getKeychainOrFail().deletePassphrase(key);
setPassphraseStored(key, false);
}
@Override
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
- keychain.changePassphrase(key, passphrase);
+ getKeychainOrFail().changePassphrase(key, passphrase);
setPassphraseStored(key, true);
}
@Override
public boolean isSupported() {
- return true;
+ return keychain.getValue() != null;
}
/**
@@ -69,7 +80,7 @@ public class KeychainManager implements KeychainAccessStrategy {
public boolean isPassphraseStored(String key) throws KeychainAccessException {
char[] storedPw = null;
try {
- storedPw = keychain.loadPassphrase(key);
+ storedPw = getKeychainOrFail().loadPassphrase(key);
return storedPw != null;
} finally {
if (storedPw != null) {
@@ -77,14 +88,13 @@ public class KeychainManager implements KeychainAccessStrategy {
}
}
}
-
+
private void setPassphraseStored(String key, boolean value) {
BooleanProperty property = passphraseStoredProperties.getIfPresent(key);
if (property != null) {
if (Platform.isFxApplicationThread()) {
property.set(value);
} else {
- LOG.warn("");
Platform.runLater(() -> property.set(value));
}
}
@@ -99,7 +109,7 @@ public class KeychainManager implements KeychainAccessStrategy {
*
* @param key The key to look up
* @return An observable property which is true when it almost certain that a password for key is stored.
- * @see #isPassphraseStored(String)
+ * @see #isPassphraseStored(String)
*/
public ReadOnlyBooleanProperty getPassphraseStoredProperty(String key) {
return passphraseStoredProperties.getUnchecked(key);
@@ -107,7 +117,6 @@ public class KeychainManager implements KeychainAccessStrategy {
private BooleanProperty createStoredPassphraseProperty(String key) {
try {
- LOG.warn("LOAD"); // TODO remove
return new SimpleBooleanProperty(isPassphraseStored(key));
} catch (KeychainAccessException e) {
return new SimpleBooleanProperty(false);
diff --git a/main/commons/src/main/java/org/cryptomator/common/keychain/KeychainModule.java b/main/commons/src/main/java/org/cryptomator/common/keychain/KeychainModule.java
new file mode 100644
index 000000000..9ac343d36
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/keychain/KeychainModule.java
@@ -0,0 +1,44 @@
+package org.cryptomator.common.keychain;
+
+import dagger.Module;
+import dagger.Provides;
+import org.cryptomator.common.settings.Settings;
+import org.cryptomator.integrations.keychain.KeychainAccessProvider;
+
+import javax.inject.Singleton;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.ObjectExpression;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Module
+public class KeychainModule {
+
+ @Provides
+ @Singleton
+ static Set> provideAvailableKeychainAccessProviderFactories() {
+ return ServiceLoader.load(KeychainAccessProvider.class).stream().collect(Collectors.toUnmodifiableSet());
+ }
+
+ @Provides
+ @Singleton
+ static Set provideSupportedKeychainAccessProviders(Set> availableFactories) {
+ return availableFactories.stream() //
+ .map(ServiceLoader.Provider::get) //
+ .filter(KeychainAccessProvider::isSupported) //
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ @Provides
+ @Singleton
+ static ObjectExpression provideKeychainAccessProvider(Settings settings, Set providers) {
+ return Bindings.createObjectBinding(() -> {
+ var selectedProviderClass = settings.keychainBackend().get().getProviderClass();
+ var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny();
+ var fallbackProvider = providers.stream().findAny().orElse(null);
+ return selectedProvider.orElse(fallbackProvider);
+ }, settings.keychainBackend());
+ }
+
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/keychain/NoKeychainAccessProviderException.java b/main/commons/src/main/java/org/cryptomator/common/keychain/NoKeychainAccessProviderException.java
new file mode 100644
index 000000000..c14b81076
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/keychain/NoKeychainAccessProviderException.java
@@ -0,0 +1,13 @@
+package org.cryptomator.common.keychain;
+
+import org.cryptomator.integrations.keychain.KeychainAccessException;
+
+/**
+ * Thrown by {@link KeychainManager} if attempted to access a keychain despite no supported keychain access provider being available.
+ */
+public class NoKeychainAccessProviderException extends KeychainAccessException {
+
+ public NoKeychainAccessProviderException() {
+ super("Did not find any supported keychain access provider.");
+ }
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/mountpoint/AvailableDriveLetterChooser.java b/main/commons/src/main/java/org/cryptomator/common/mountpoint/AvailableDriveLetterChooser.java
new file mode 100644
index 000000000..7634601fa
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/mountpoint/AvailableDriveLetterChooser.java
@@ -0,0 +1,36 @@
+package org.cryptomator.common.mountpoint;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.vaults.Volume;
+import org.cryptomator.common.vaults.WindowsDriveLetters;
+
+import javax.inject.Inject;
+import java.nio.file.Path;
+import java.util.Optional;
+
+public class AvailableDriveLetterChooser implements MountPointChooser {
+
+ public static final int PRIORITY = 200;
+
+ private final WindowsDriveLetters windowsDriveLetters;
+
+ @Inject
+ public AvailableDriveLetterChooser(WindowsDriveLetters windowsDriveLetters) {
+ this.windowsDriveLetters = windowsDriveLetters;
+ }
+
+ @Override
+ public boolean isApplicable(Volume caller) {
+ return SystemUtils.IS_OS_WINDOWS;
+ }
+
+ @Override
+ public Optional chooseMountPoint(Volume caller) {
+ return this.windowsDriveLetters.getAvailableDriveLetterPath();
+ }
+
+ @Override
+ public int getPriority() {
+ return PRIORITY;
+ }
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomDriveLetterChooser.java b/main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomDriveLetterChooser.java
new file mode 100644
index 000000000..ac9e5b716
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomDriveLetterChooser.java
@@ -0,0 +1,37 @@
+package org.cryptomator.common.mountpoint;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.settings.VaultSettings;
+import org.cryptomator.common.vaults.Volume;
+
+import javax.inject.Inject;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+public class CustomDriveLetterChooser implements MountPointChooser {
+
+ public static final int PRIORITY = 100;
+
+ private final VaultSettings vaultSettings;
+
+ @Inject
+ public CustomDriveLetterChooser(VaultSettings vaultSettings) {
+ this.vaultSettings = vaultSettings;
+ }
+
+ @Override
+ public boolean isApplicable(Volume caller) {
+ return SystemUtils.IS_OS_WINDOWS;
+ }
+
+ @Override
+ public Optional chooseMountPoint(Volume caller) {
+ return this.vaultSettings.getWinDriveLetter().map(letter -> letter.charAt(0) + ":\\").map(Paths::get);
+ }
+
+ @Override
+ public int getPriority() {
+ return PRIORITY;
+ }
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java
new file mode 100644
index 000000000..6a86c654a
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java
@@ -0,0 +1,101 @@
+package org.cryptomator.common.mountpoint;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Environment;
+import org.cryptomator.common.settings.VaultSettings;
+import org.cryptomator.common.settings.VolumeImpl;
+import org.cryptomator.common.vaults.Volume;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+public class CustomMountPointChooser implements MountPointChooser {
+
+ public static final int PRIORITY = 0;
+
+ private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class);
+
+ private final VaultSettings vaultSettings;
+ private final Environment environment;
+
+ @Inject
+ public CustomMountPointChooser(VaultSettings vaultSettings, Environment environment) {
+ this.vaultSettings = vaultSettings;
+ this.environment = environment;
+ }
+
+ @Override
+ public boolean isApplicable(Volume caller) {
+ //Disable if useExperimentalFuse is required (Win + Fuse), but set to false
+ return caller.getImplementationType() != VolumeImpl.FUSE || !SystemUtils.IS_OS_WINDOWS || environment.useExperimentalFuse();
+ }
+
+ @Override
+ public Optional chooseMountPoint(Volume caller) {
+ //VaultSettings#getCustomMountPath already checks whether the saved custom mountpoint should be used
+ return this.vaultSettings.getCustomMountPath().map(Paths::get);
+ }
+
+ @Override
+ public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
+ switch (caller.getMountPointRequirement()) {
+ case PARENT_NO_MOUNT_POINT -> prepareParentNoMountPoint(mountPoint);
+ case EMPTY_MOUNT_POINT -> prepareEmptyMountPoint(mountPoint);
+ case NONE -> {
+ //Requirement "NONE" doesn't make any sense here.
+ //No need to prepare/verify a Mountpoint without requiring one...
+ throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
+ }
+ default -> {
+ //Currently the case for "PARENT_OPT_MOUNT_POINT"
+ throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
+ }
+ }
+ LOG.debug("Successfully checked custom mount point: {}", mountPoint);
+ return false;
+ }
+
+ private void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException {
+ //This the case on Windows when using FUSE
+ //See https://github.com/billziss-gh/winfsp/issues/320
+ Path parent = mountPoint.getParent();
+ if (!Files.isDirectory(parent)) {
+ throw new InvalidMountPointException(new NotDirectoryException(parent.toString()));
+ }
+ //We must use #notExists() here because notExists =/= !exists (see docs)
+ if (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
+ //File exists OR can't be determined
+ throw new InvalidMountPointException(new FileAlreadyExistsException(mountPoint.toString()));
+ }
+ }
+
+ private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException {
+ //This is the case for Windows when using Dokany and for Linux and Mac
+ if (!Files.isDirectory(mountPoint)) {
+ throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString()));
+ }
+ try (DirectoryStream ds = Files.newDirectoryStream(mountPoint)) {
+ if (ds.iterator().hasNext()) {
+ throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString()));
+ }
+ } catch (IOException exception) {
+ throw new InvalidMountPointException("IOException while checking folder content", exception);
+ }
+ }
+
+ @Override
+ public int getPriority() {
+ return PRIORITY;
+ }
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/mountpoint/InvalidMountPointException.java b/main/commons/src/main/java/org/cryptomator/common/mountpoint/InvalidMountPointException.java
new file mode 100644
index 000000000..b0c2fbd7a
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/mountpoint/InvalidMountPointException.java
@@ -0,0 +1,16 @@
+package org.cryptomator.common.mountpoint;
+
+public class InvalidMountPointException extends Exception {
+
+ public InvalidMountPointException(String message) {
+ super(message);
+ }
+
+ public InvalidMountPointException(Throwable cause) {
+ super(cause);
+ }
+
+ public InvalidMountPointException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java b/main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java
new file mode 100644
index 000000000..d91fd0041
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java
@@ -0,0 +1,64 @@
+package org.cryptomator.common.mountpoint;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+
+public class IrregularUnmountCleaner {
+
+ public static Logger LOG = LoggerFactory.getLogger(IrregularUnmountCleaner.class);
+
+ public static void removeIrregularUnmountDebris(Path dirContainingMountPoints) {
+ IOException cleanupFailed = new IOException("Cleanup failed");
+
+ try {
+ LOG.debug("Performing cleanup of mountpoint dir {}.", dirContainingMountPoints);
+ for (Path p : Files.newDirectoryStream(dirContainingMountPoints)) {
+ try {
+ var attr = Files.readAttributes(p, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
+ if (attr.isOther() && attr.isDirectory()) { // yes, this is possible with windows junction points -.-
+ Files.delete(p);
+ } else if (attr.isDirectory()) {
+ deleteEmptyDir(p);
+ } else if (attr.isSymbolicLink()) {
+ deleteDeadLink(p);
+ } else {
+ LOG.debug("Found non-directory element in mountpoint dir: {}", p);
+ }
+ } catch (IOException e) {
+ cleanupFailed.addSuppressed(e);
+ }
+ }
+
+ if (cleanupFailed.getSuppressed().length > 0) {
+ throw cleanupFailed;
+ }
+ } catch (IOException e) {
+ LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e);
+ }
+
+ }
+
+ private static void deleteEmptyDir(Path dir) throws IOException {
+ assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
+ try {
+ Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents)
+ } catch (DirectoryNotEmptyException e) {
+ LOG.info("Found non-empty directory in mountpoint dir: {}", dir);
+ }
+ }
+
+ private static void deleteDeadLink(Path symlink) throws IOException {
+ assert Files.isSymbolicLink(symlink);
+ if (Files.notExists(symlink)) { // following link: target does not exist
+ Files.delete(symlink);
+ }
+ }
+
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java b/main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java
new file mode 100644
index 000000000..2c40bb74a
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java
@@ -0,0 +1,169 @@
+package org.cryptomator.common.mountpoint;
+
+import com.google.common.base.Preconditions;
+import org.cryptomator.common.vaults.Volume;
+
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.SortedSet;
+
+/**
+ * Base interface for the Mountpoint-Choosing-Operation that results in the choice and
+ * preparation of a mountpoint or an exception otherwise.
+ *
All MountPointChoosers (MPCs) need to implement this class and must be added to
+ * the pool of possible MPCs by the {@link MountPointChooserModule MountPointChooserModule.}
+ * The MountPointChooserModule will sort them according to their {@link #getPriority() priority.}
+ * The priority must be defined by the developer to reflect a useful execution order.
+ * A specific priority must not be assigned to more than one MPC at a time;
+ * the result of having two MPCs with equal priority is undefined.
+ *
+ *
MPCs are executed by a {@link Volume} in ascending order of their priority
+ * (smaller priorities are tried first) to find and prepare a suitable mountpoint for the volume.
+ * The volume has access to a {@link SortedSet} of MPCs in this specific order,
+ * that is provided by the Module. The Set contains all available Choosers, even if they
+ * are not {@link #isApplicable(Volume) applicable} for the Vault/Volume. The Volume must
+ * check whether a MPC is applicable by invoking {@code #isApplicable(Volume)} on it
+ * before calling {@code #chooseMountPoint(Volume)}.
+ *
+ *
At execution of a MPC {@link #chooseMountPoint(Volume)} is called to choose a mountpoint
+ * according to the MPC's strategy. The strategy can involve reading configs,
+ * searching the filesystem, processing user-input or similar operations.
+ * If {@code #chooseMountPoint(Volume)} returns a non-null path (everything but
+ * {@linkplain Optional#empty()}) the MPC's {@link #prepare(Volume, Path)} method is called and the
+ * MountPoint is verified and/or prepared. In this case no other MPC's will be called for
+ * this volume, even if {@code #prepare(Volume, Path)} fails.
+ *
+ *
If {@code #chooseMountPoint(Volume)} yields no result, the next MPC is executed
+ * without first calling the {@code #prepare(Volume, Path)} method of the current MPC.
+ * This is repeated until
+ *
+ *
either a mountpoint is returned by {@code #chooseMountPoint(Volume)}
+ * and {@code #prepare(Volume, Path)} succeeds or fails, ending the entire operation
+ *
or no MPC remains and an {@link InvalidMountPointException} is thrown.
+ *
+ * If the {@code #prepare(Volume, Path)} method of a MPC fails, the entire
+ * Mountpoint-Choosing-Operation is aborted and the method should do all necessary cleanup
+ * before throwing the exception.
+ * If the preparation succeeds {@link #cleanup(Volume, Path)} can be used after unmount to do any
+ * remaining cleanup.
+ */
+public interface MountPointChooser extends Comparable {
+
+ /**
+ * Called by the {@link Volume} to determine whether this MountPointChooser is
+ * applicable for mounting the Vault/Volume, especially with regard to the
+ * current system configuration and particularities of the Volume type.
+ *
+ *
Developers should override this method to check for system configurations
+ * that are unsuitable for this MPC.
+ *
+ * @param caller The Volume that is calling the method to determine applicability of the MPC
+ * @return a boolean flag; true if applicable, else false.
+ * @see #chooseMountPoint(Volume)
+ */
+ boolean isApplicable(Volume caller);
+
+ /**
+ * Called by a {@link Volume} to choose a mountpoint according to the
+ * MountPointChoosers strategy.
+ *
+ *
This method must only be called for MPCs that were deemed
+ * {@link #isApplicable(Volume) applicable} by the {@link Volume Volume.}
+ * Developers should override this method to find or extract a mountpoint for
+ * the volume without preparing it. Preparation should be done by
+ * {@link #prepare(Volume, Path)} instead.
+ * Exceptions in this method should be handled gracefully and result in returning
+ * {@link Optional#empty()} instead of throwing an exception.
+ *
+ * @param caller The Volume that is calling the method to choose a mountpoint
+ * @return the chosen path or {@link Optional#empty()} if an exception occurred
+ * or no mountpoint could be found.
+ * @see #isApplicable(Volume)
+ * @see #prepare(Volume, Path)
+ */
+ Optional chooseMountPoint(Volume caller);
+
+ /**
+ * Called by a {@link Volume} to prepare and/or verify the chosen mountpoint.
+ * This method is only called if the {@link #chooseMountPoint(Volume)} method
+ * of the same MountPointChooser returned a path.
+ *
+ *
Developers should override this method to prepare the mountpoint for
+ * the volume and check for any obstacles that could hinder the mount operation.
+ * The mountpoint is deemed "prepared" if it can be used to mount a volume
+ * without any further filesystem actions or user interaction. If this is not possible,
+ * this method should fail. In other words: This method should not return without
+ * either failing or finalizing the preparation of the mountpoint.
+ * Generally speaking exceptions should be wrapped as
+ * {@link InvalidMountPointException} to allow efficient handling by the caller.
+ *
+ *
Often the preparation of a mountpoint involves creating files or others
+ * actions that require cleaning up after the volume is unmounted.
+ * In this case developers should override the {@link #cleanup(Volume, Path)}
+ * method and return {@code true} to the volume to indicate that the
+ * {@code #cleanup} method of this MPC should be called after unmount.
+ *
+ *
Please note: If this method fails the entire
+ * Mountpoint-Choosing-Operation is aborted without calling
+ * {@link #cleanup(Volume, Path)} or any other MPCs. Therefore this method should
+ * do all necessary cleanup before throwing the exception.
+ *
+ * @param caller The Volume that is calling the method to prepare a mountpoint
+ * @param mountPoint the mountpoint chosen by {@link #chooseMountPoint(Volume)}
+ * @return a boolean flag; true if cleanup is needed, false otherwise
+ * @throws InvalidMountPointException if the preparation fails
+ * @see #chooseMountPoint(Volume)
+ * @see #cleanup(Volume, Path)
+ */
+ default boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
+ return false; //NO-OP
+ }
+
+ /**
+ * Called by a {@link Volume} to do any cleanup needed after unmount.
+ *
+ *
This method is only called if the {@link #prepare(Volume, Path)} method
+ * of the same MountPointChooser returned {@code true}. Typically developers want to
+ * delete any files created prior to mount or do similar tasks.
+ * Exceptions in this method should be handled gracefully.
+ *
+ * @param caller The Volume that is calling the method to cleanup the prepared mountpoint
+ * @param mountPoint the mountpoint that was prepared by {@link #prepare(Volume, Path)}
+ * @see #prepare(Volume, Path)
+ */
+ default void cleanup(Volume caller, Path mountPoint) {
+ //NO-OP
+ }
+
+ /**
+ * Called by the {@link MountPointChooserModule} to sort the available MPCs
+ * and determine their execution order.
+ * The priority must be defined by the developer to reflect a useful execution order.
+ * MPCs with lower priorities will be placed at lower indices in the resulting
+ * {@link SortedSet} and will be executed with higher probability.
+ * A specific priority must not be assigned to more than one MPC at a time;
+ * the result of having two MPCs with equal priority is undefined.
+ *
+ * @return the priority of this MPC.
+ */
+ int getPriority();
+
+ /**
+ * Called by the {@link Volume} to determine the execution order of the registered MPCs.
+ * Implementations usually may not override this method. This default implementation
+ * sorts the MPCs in ascending order of their {@link #getPriority() priority.}
+ *
+ * Original description:
+ *
{@inheritDoc}
+ *
+ * @implNote This default implementation sorts the MPCs in ascending order
+ * of their {@link #getPriority() priority.}
+ */
+ @Override
+ default int compareTo(MountPointChooser other) {
+ Preconditions.checkNotNull(other, "Other must not be null!");
+
+ //Sort by priority (ascending order)
+ return Integer.compare(this.getPriority(), other.getPriority());
+ }
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java b/main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java
new file mode 100644
index 000000000..09789dacb
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java
@@ -0,0 +1,50 @@
+package org.cryptomator.common.mountpoint;
+
+import com.google.common.collect.ImmutableSortedSet;
+import dagger.Binds;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.IntoSet;
+import org.cryptomator.common.vaults.PerVault;
+
+import javax.inject.Named;
+import java.util.Set;
+import java.util.SortedSet;
+
+/**
+ * Dagger-Module for {@link MountPointChooser MountPointChoosers.}
+ * See there for additional information.
+ *
+ * @see MountPointChooser
+ */
+@Module
+public abstract class MountPointChooserModule {
+
+ @Binds
+ @IntoSet
+ @PerVault
+ public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser);
+
+ @Binds
+ @IntoSet
+ @PerVault
+ public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser);
+
+ @Binds
+ @IntoSet
+ @PerVault
+ public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);
+
+ @Binds
+ @IntoSet
+ @PerVault
+ public abstract MountPointChooser bindTemporaryMountPointChooser(TemporaryMountPointChooser chooser);
+
+ @Provides
+ @PerVault
+ @Named("orderedMountPointChoosers")
+ public static SortedSet provideOrderedMountPointChoosers(Set choosers) {
+ //Sort by natural order. The natural order is defined by MountPointChooser#compareTo
+ return ImmutableSortedSet.copyOf(choosers);
+ }
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java b/main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java
new file mode 100644
index 000000000..17c960b1e
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java
@@ -0,0 +1,121 @@
+package org.cryptomator.common.mountpoint;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Environment;
+import org.cryptomator.common.settings.VaultSettings;
+import org.cryptomator.common.vaults.Volume;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+public class TemporaryMountPointChooser implements MountPointChooser {
+
+ public static final int PRIORITY = 300;
+
+ private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
+ private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
+
+ private final VaultSettings vaultSettings;
+ private final Environment environment;
+
+ @Inject
+ public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment) {
+ this.vaultSettings = vaultSettings;
+ this.environment = environment;
+ }
+
+ @Override
+ public boolean isApplicable(Volume caller) {
+ if (this.environment.getMountPointsDir().isEmpty()) {
+ LOG.warn("\"cryptomator.mountPointsDir\" is not set to a valid path!");
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public Optional chooseMountPoint(Volume caller) {
+ return this.environment.getMountPointsDir().map(this::choose);
+ }
+
+ private Path choose(Path parent) {
+ String basename = this.vaultSettings.mountName().get();
+ //regular
+ Path mountPoint = parent.resolve(basename);
+ if (Files.notExists(mountPoint)) {
+ return mountPoint;
+ }
+ //with id
+ mountPoint = parent.resolve(basename + " (" +vaultSettings.getId() + ")");
+ if (Files.notExists(mountPoint)) {
+ return mountPoint;
+ }
+ //with id and count
+ for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
+ mountPoint = parent.resolve(basename + "_(" +vaultSettings.getId() + ")_"+i);
+ if (Files.notExists(mountPoint)) {
+ return mountPoint;
+ }
+ }
+ LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parent, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
+ return null;
+ }
+
+ @Override
+ public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
+ // https://github.com/osxfuse/osxfuse/issues/306#issuecomment-245114592:
+ // In order to allow non-admin users to mount FUSE volumes in `/Volumes`,
+ // starting with version 3.5.0, FUSE will create non-existent mount points automatically.
+ if (SystemUtils.IS_OS_MAC && mountPoint.getParent().equals(Paths.get("/Volumes"))) {
+ return false;
+ }
+
+ try {
+ switch (caller.getMountPointRequirement()) {
+ case PARENT_NO_MOUNT_POINT -> {
+ Files.createDirectories(mountPoint.getParent());
+ LOG.debug("Successfully created folder for mount point: {}", mountPoint);
+ return false;
+ }
+ case EMPTY_MOUNT_POINT -> {
+ Files.createDirectories(mountPoint);
+ LOG.debug("Successfully created mount point: {}", mountPoint);
+ return true;
+ }
+ case NONE -> {
+ //Requirement "NONE" doesn't make any sense here.
+ //No need to prepare/verify a Mountpoint without requiring one...
+ throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
+ }
+ default -> {
+ //Currently the case for "PARENT_OPT_MOUNT_POINT"
+ throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
+ }
+ }
+ } catch (IOException exception) {
+ throw new InvalidMountPointException("IOException while preparing mountpoint", exception);
+ }
+ }
+
+ @Override
+ public void cleanup(Volume caller, Path mountPoint) {
+ try {
+ Files.delete(mountPoint);
+ LOG.debug("Successfully deleted mount point: {}", mountPoint);
+ } catch (IOException e) {
+ LOG.warn("Could not delete mount point: {}", e.getMessage());
+ }
+ }
+
+ @Override
+ public int getPriority() {
+ return PRIORITY;
+ }
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java b/main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java
new file mode 100644
index 000000000..65f869a12
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java
@@ -0,0 +1,19 @@
+package org.cryptomator.common.settings;
+
+public enum KeychainBackend {
+ GNOME("org.cryptomator.linux.keychain.SecretServiceKeychainAccess"),
+ KDE("org.cryptomator.linux.keychain.KDEWalletKeychainAccess"),
+ MAC_SYSTEM_KEYCHAIN("org.cryptomator.macos.keychain.MacSystemKeychainAccess"),
+ WIN_SYSTEM_KEYCHAIN("org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess");
+
+ private final String providerClass;
+
+ KeychainBackend(String providerClass) {
+ this.providerClass = providerClass;
+ }
+
+ public String getProviderClass() {
+ return providerClass;
+ }
+
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java
index 16d16b01d..e50391d2d 100644
--- a/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java
+++ b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java
@@ -8,6 +8,8 @@
******************************************************************************/
package org.cryptomator.common.settings;
+import org.apache.commons.lang3.SystemUtils;
+
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
@@ -20,7 +22,6 @@ import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.NodeOrientation;
-
import java.util.function.Consumer;
public class Settings {
@@ -34,8 +35,9 @@ public class Settings {
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV;
public static final boolean DEFAULT_DEBUG_MODE = false;
- public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
+ public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = SystemUtils.IS_OS_WINDOWS ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
+ public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = SystemUtils.IS_OS_WINDOWS ? KeychainBackend.WIN_SYSTEM_KEYCHAIN : SystemUtils.IS_OS_MAC ? KeychainBackend.MAC_SYSTEM_KEYCHAIN : KeychainBackend.GNOME;
public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
private static final String DEFAULT_LICENSE_KEY = "";
@@ -49,6 +51,7 @@ public class Settings {
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
private final ObjectProperty preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
private final ObjectProperty theme = new SimpleObjectProperty<>(DEFAULT_THEME);
+ private final ObjectProperty keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND);
private final ObjectProperty userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
@@ -68,6 +71,7 @@ public class Settings {
debugMode.addListener(this::somethingChanged);
preferredVolumeImpl.addListener(this::somethingChanged);
theme.addListener(this::somethingChanged);
+ keychainBackend.addListener(this::somethingChanged);
userInterfaceOrientation.addListener(this::somethingChanged);
licenseKey.addListener(this::somethingChanged);
}
@@ -75,7 +79,7 @@ public class Settings {
void setSaveCmd(Consumer saveCmd) {
this.saveCmd = saveCmd;
}
-
+
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
this.save();
}
@@ -99,7 +103,7 @@ public class Settings {
public BooleanProperty checkForUpdates() {
return checkForUpdates;
}
-
+
public BooleanProperty startHidden() {
return startHidden;
}
@@ -128,6 +132,8 @@ public class Settings {
return theme;
}
+ public ObjectProperty keychainBackend() { return keychainBackend; }
+
public ObjectProperty userInterfaceOrientation() {
return userInterfaceOrientation;
}
diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
index 395034f56..3b06b8a0b 100644
--- a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
+++ b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
@@ -9,10 +9,10 @@ import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
-import javafx.geometry.NodeOrientation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javafx.geometry.NodeOrientation;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -38,6 +38,7 @@ public class SettingsJsonAdapter extends TypeAdapter {
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
out.name("theme").value(value.theme().get().name());
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
+ out.name("keychainBackend").value(value.keychainBackend().get().name());
out.name("licenseKey").value(value.licenseKey().get());
out.endObject();
}
@@ -69,6 +70,7 @@ public class SettingsJsonAdapter extends TypeAdapter {
case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
+ case "keychainBackend" -> settings.keychainBackend().set(parseKeychainBackend(in.nextString()));
case "licenseKey" -> settings.licenseKey().set(in.nextString());
default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name);
@@ -108,6 +110,15 @@ public class SettingsJsonAdapter extends TypeAdapter {
}
}
+ private KeychainBackend parseKeychainBackend(String backendName) {
+ try {
+ return KeychainBackend.valueOf(backendName.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ LOG.warn("Invalid keychain backend {}. Defaulting to {}.", backendName, Settings.DEFAULT_KEYCHAIN_BACKEND);
+ return Settings.DEFAULT_KEYCHAIN_BACKEND;
+ }
+ }
+
private NodeOrientation parseUiOrientation(String uiOrientationName) {
try {
return NodeOrientation.valueOf(uiOrientationName.toUpperCase());
diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java
index b5b2ffac6..7de22753a 100644
--- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java
+++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java
@@ -5,8 +5,10 @@
*******************************************************************************/
package org.cryptomator.common.settings;
+import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
+
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
@@ -18,12 +20,12 @@ import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
-import org.apache.commons.lang3.StringUtils;
-
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
+import java.util.Set;
+import java.util.stream.Collectors;
/**
* The settings specific to a single vault.
@@ -76,22 +78,16 @@ public class VaultSettings {
//visible for testing
String normalizeDisplayName() {
- String normalizedMountName = StringUtils.stripAccents(displayName.get());
- StringBuilder builder = new StringBuilder();
- for (char c : normalizedMountName.toCharArray()) {
- if (Character.isWhitespace(c)) {
- if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
- builder.append('_');
- }
- } else if (c < 127 && Character.isLetterOrDigit(c)) {
- builder.append(c);
- } else {
- if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
- builder.append('_');
- }
- }
+ var original = displayName.getValueSafe();
+ if (original.isBlank() || ".".equals(original) || "..".equals(original)) {
+ return "_";
}
- return builder.toString();
+
+ // replace whitespaces (tabs, linebreaks, ...) by simple space (0x20)
+ var withoutFancyWhitespaces = CharMatcher.whitespace().collapseFrom(original, ' ');
+
+ // replace control chars as well as chars that aren't allowed in file names on standard file systems by underscore
+ return CharMatcher.anyOf("<>:\"/\\|?*").or(CharMatcher.javaIsoControl()).collapseFrom(withoutFancyWhitespaces, '_');
}
/* Getter/Setter */
@@ -116,6 +112,14 @@ public class VaultSettings {
return winDriveLetter;
}
+ public Optional getWinDriveLetter() {
+ String current = this.winDriveLetter.get();
+ if (!Strings.isNullOrEmpty(current)) {
+ return Optional.of(current);
+ }
+ return Optional.empty();
+ }
+
public BooleanProperty unlockAfterStartup() {
return unlockAfterStartup;
}
diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java b/main/commons/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java
index 0dcb4ac9d..a72d11968 100644
--- a/main/commons/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java
+++ b/main/commons/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java
@@ -7,7 +7,8 @@ public enum WebDavUrlScheme {
private final String prefix;
private final String displayName;
- WebDavUrlScheme(String prefix, String displayName) {this.prefix = prefix;
+ WebDavUrlScheme(String prefix, String displayName) {
+ this.prefix = prefix;
this.displayName = displayName;
}
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java b/main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java
new file mode 100644
index 000000000..91c6e22e4
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java
@@ -0,0 +1,60 @@
+package org.cryptomator.common.vaults;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+import org.cryptomator.common.mountpoint.InvalidMountPointException;
+import org.cryptomator.common.mountpoint.MountPointChooser;
+
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public abstract class AbstractVolume implements Volume {
+
+ private final SortedSet choosers;
+
+ protected Path mountPoint;
+
+ //Cleanup
+ private boolean cleanupRequired;
+ private MountPointChooser usedChooser;
+
+ public AbstractVolume(SortedSet choosers) {
+ this.choosers = choosers;
+ }
+
+ protected Path determineMountPoint() throws InvalidMountPointException {
+ SortedSet checkedChoosers = new TreeSet<>(); //Natural order
+ for (MountPointChooser chooser : this.choosers) {
+ if (!chooser.isApplicable(this)) {
+ continue;
+ }
+
+ Optional chosenPath = chooser.chooseMountPoint(this);
+ checkedChoosers.add(chooser); //Consider a chooser checked if it's #chooseMountPoint() method was called
+ if (chosenPath.isEmpty()) {
+ //Chooser was applicable, but couldn't find a feasible mountpoint
+ continue;
+ }
+ this.cleanupRequired = chooser.prepare(this, chosenPath.get()); //Fail entirely if an Exception occurs
+ this.usedChooser = chooser;
+ return chosenPath.get();
+ }
+ //SortedSet#stream() should return a sorted stream (that's what it's docs and the docs of #spliterator() say, even if they are not 100% clear for me.)
+ //We want to keep that order, that's why we use ImmutableSet#toImmutableSet() to collect (even if it doesn't implement SortedSet, it's docs promise use encounter ordering.)
+ String checked = Joiner.on(", ").join(checkedChoosers.stream().map((mpc) -> mpc.getClass().getTypeName()).collect(ImmutableSet.toImmutableSet()));
+ throw new InvalidMountPointException(String.format("No feasible MountPoint found! Checked %s", checked.isBlank() ? "" : checked));
+ }
+
+ protected void cleanupMountPoint() {
+ if (this.cleanupRequired) {
+ this.usedChooser.cleanup(this, this.mountPoint);
+ }
+ }
+
+ @Override
+ public Optional getMountPoint() {
+ return Optional.ofNullable(mountPoint);
+ }
+}
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/DefaultMountFlags.java b/main/commons/src/main/java/org/cryptomator/common/vaults/DefaultMountFlags.java
index ea2cdfa0a..68b61688b 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/DefaultMountFlags.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/DefaultMountFlags.java
@@ -10,4 +10,5 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Retention(RUNTIME)
public @interface DefaultMountFlags {
+
}
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java b/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java
index 117f888c9..e91c9f86d 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java
@@ -1,7 +1,9 @@
package org.cryptomator.common.vaults;
-import com.google.common.base.Strings;
+import org.cryptomator.common.mountpoint.InvalidMountPointException;
+import org.cryptomator.common.mountpoint.MountPointChooser;
import org.cryptomator.common.settings.VaultSettings;
+import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.dokany.Mount;
import org.cryptomator.frontend.dokany.MountFactory;
@@ -10,44 +12,37 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
-import java.io.IOException;
-import java.nio.file.DirectoryNotEmptyException;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.NotDirectoryException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Optional;
+import javax.inject.Named;
+import java.util.SortedSet;
import java.util.concurrent.ExecutorService;
-public class DokanyVolume implements Volume {
+public class DokanyVolume extends AbstractVolume {
private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class);
- private static final String FS_TYPE_NAME = "Cryptomator File System";
+ private static final String FS_TYPE_NAME = "CryptomatorFS";
private final VaultSettings vaultSettings;
private final MountFactory mountFactory;
- private final WindowsDriveLetters windowsDriveLetters;
+
private Mount mount;
- private Path mountPoint;
@Inject
- public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, WindowsDriveLetters windowsDriveLetters) {
+ public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, @Named("orderedMountPointChoosers") SortedSet choosers) {
+ super(choosers);
this.vaultSettings = vaultSettings;
this.mountFactory = new MountFactory(executorService);
- this.windowsDriveLetters = windowsDriveLetters;
}
@Override
- public boolean isSupported() {
- return DokanyVolume.isSupportedStatic();
+ public VolumeImpl getImplementationType() {
+ return VolumeImpl.DOKANY;
}
@Override
- public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException, IOException {
+ public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
this.mountPoint = determineMountPoint();
- String mountName = vaultSettings.displayName().get();
+ String mountName = vaultSettings.mountName().get();
try {
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip());
} catch (MountFailedException e) {
@@ -58,36 +53,6 @@ public class DokanyVolume implements Volume {
}
}
- private Path determineMountPoint() throws VolumeException, IOException {
- Optional optionalCustomMountPoint = vaultSettings.getCustomMountPath();
- if (optionalCustomMountPoint.isPresent()) {
- Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
- checkProvidedMountPoint(customMountPoint);
- return customMountPoint;
- } else if (!Strings.isNullOrEmpty(vaultSettings.winDriveLetter().get())) {
- return Path.of(vaultSettings.winDriveLetter().get().charAt(0) + ":\\");
- } else {
- //auto assign drive letter
- if (!windowsDriveLetters.getAvailableDriveLetters().isEmpty()) {
- return Path.of(windowsDriveLetters.getAvailableDriveLetters().iterator().next() + ":\\");
- } else {
- //TODO: Error Handling
- throw new VolumeException("No free drive letter available.");
- }
- }
- }
-
- private void checkProvidedMountPoint(Path mountPoint) throws IOException {
- if (!Files.isDirectory(mountPoint)) {
- throw new NotDirectoryException(mountPoint.toString());
- }
- try (DirectoryStream ds = Files.newDirectoryStream(mountPoint)) {
- if (ds.iterator().hasNext()) {
- throw new DirectoryNotEmptyException(mountPoint.toString());
- }
- }
- }
-
@Override
public void reveal() throws VolumeException {
boolean success = mount.reveal();
@@ -99,11 +64,17 @@ public class DokanyVolume implements Volume {
@Override
public void unmount() {
mount.close();
+ cleanupMountPoint();
}
@Override
- public Optional getMountPoint() {
- return Optional.ofNullable(mountPoint);
+ public boolean isSupported() {
+ return DokanyVolume.isSupportedStatic();
+ }
+
+ @Override
+ public MountPointRequirement getMountPointRequirement() {
+ return MountPointRequirement.EMPTY_MOUNT_POINT;
}
public static boolean isSupportedStatic() {
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java b/main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java
index 4b6918be9..76a83983b 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java
@@ -2,8 +2,9 @@ package org.cryptomator.common.vaults;
import com.google.common.base.Splitter;
import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.Environment;
-import org.cryptomator.common.settings.VaultSettings;
+import org.cryptomator.common.mountpoint.InvalidMountPointException;
+import org.cryptomator.common.mountpoint.MountPointChooser;
+import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.fuse.mount.CommandFailedException;
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
@@ -15,95 +16,36 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
-import java.io.IOException;
-import java.nio.file.DirectoryNotEmptyException;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.NotDirectoryException;
+import javax.inject.Named;
import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Optional;
+import java.util.SortedSet;
-public class FuseVolume implements Volume {
+public class FuseVolume extends AbstractVolume {
private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class);
- private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
- private static final boolean IS_MAC = System.getProperty("os.name").toLowerCase().contains("mac");
- private final VaultSettings vaultSettings;
- private final Environment environment;
-
- private Mount fuseMnt;
- private Path mountPoint;
- private boolean createdTemporaryMountPoint;
+ private Mount mount;
@Inject
- public FuseVolume(VaultSettings vaultSettings, Environment environment) {
- this.vaultSettings = vaultSettings;
- this.environment = environment;
+ public FuseVolume(@Named("orderedMountPointChoosers") SortedSet choosers) {
+ super(choosers);
}
@Override
- public void mount(CryptoFileSystem fs, String mountFlags) throws IOException, FuseNotSupportedException, VolumeException {
- Optional optionalCustomMountPoint = vaultSettings.getCustomMountPath();
- if (optionalCustomMountPoint.isPresent()) {
- Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
- checkProvidedMountPoint(customMountPoint);
- this.mountPoint = customMountPoint;
- LOG.debug("Successfully checked custom mount point: {}", mountPoint);
- } else {
- this.mountPoint = prepareTemporaryMountPoint();
- LOG.debug("Successfully created mount point: {}", mountPoint);
- }
+ public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
+ this.mountPoint = determineMountPoint();
+
mount(fs.getPath("/"), mountFlags);
}
- private void checkProvidedMountPoint(Path mountPoint) throws IOException {
- if (!Files.isDirectory(mountPoint)) {
- throw new NotDirectoryException(mountPoint.toString());
- }
- try (DirectoryStream ds = Files.newDirectoryStream(mountPoint)) {
- if (ds.iterator().hasNext()) {
- throw new DirectoryNotEmptyException(mountPoint.toString());
- }
- }
- }
-
- private Path prepareTemporaryMountPoint() throws IOException, VolumeException {
- Path mountPoint = chooseNonExistingTemporaryMountPoint();
- // https://github.com/osxfuse/osxfuse/issues/306#issuecomment-245114592:
- // In order to allow non-admin users to mount FUSE volumes in `/Volumes`,
- // starting with version 3.5.0, FUSE will create non-existent mount points automatically.
- if (IS_MAC && mountPoint.getParent().equals(Paths.get("/Volumes"))) {
- return mountPoint;
- } else {
- Files.createDirectories(mountPoint);
- this.createdTemporaryMountPoint = true;
- return mountPoint;
- }
- }
-
- private Path chooseNonExistingTemporaryMountPoint() throws VolumeException {
- Path parent = environment.getMountPointsDir().orElseThrow();
- String basename = vaultSettings.getId();
- for (int i = 0; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
- Path mountPoint = parent.resolve(basename + "_" + i);
- if (Files.notExists(mountPoint)) {
- return mountPoint;
- }
- }
- LOG.error("Failed to find feasible mountpoint at {}/{}_x. Giving up after {} attempts.", parent, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
- throw new VolumeException("Did not find feasible mount point.");
- }
-
private void mount(Path root, String mountFlags) throws VolumeException {
try {
Mounter mounter = FuseMountFactory.getMounter();
EnvironmentVariables envVars = EnvironmentVariables.create() //
.withFlags(splitFlags(mountFlags)).withMountPoint(mountPoint) //
.build();
- this.fuseMnt = mounter.mount(root, envVars);
- } catch (CommandFailedException e) {
+ this.mount = mounter.mount(root, envVars);
+ } catch (CommandFailedException | FuseNotSupportedException e) {
throw new VolumeException("Unable to mount Filesystem", e);
}
}
@@ -115,7 +57,7 @@ public class FuseVolume implements Volume {
@Override
public void reveal() throws VolumeException {
try {
- fuseMnt.revealInFileManager();
+ mount.revealInFileManager();
} catch (CommandFailedException e) {
LOG.debug("Revealing the vault in file manger failed: " + e.getMessage());
throw new VolumeException(e);
@@ -130,34 +72,23 @@ public class FuseVolume implements Volume {
@Override
public synchronized void unmountForced() throws VolumeException {
try {
- fuseMnt.unmountForced();
- fuseMnt.close();
+ mount.unmountForced();
+ mount.close();
} catch (CommandFailedException e) {
throw new VolumeException(e);
}
- cleanupTemporaryMountPoint();
+ cleanupMountPoint();
}
@Override
public synchronized void unmount() throws VolumeException {
try {
- fuseMnt.unmount();
- fuseMnt.close();
+ mount.unmount();
+ mount.close();
} catch (CommandFailedException e) {
throw new VolumeException(e);
}
- cleanupTemporaryMountPoint();
- }
-
- private void cleanupTemporaryMountPoint() {
- if (createdTemporaryMountPoint) {
- try {
- Files.delete(mountPoint);
- LOG.debug("Successfully deleted mount point: {}", mountPoint);
- } catch (IOException e) {
- LOG.warn("Could not delete mount point: {}", e.getMessage());
- }
- }
+ cleanupMountPoint();
}
@Override
@@ -166,12 +97,17 @@ public class FuseVolume implements Volume {
}
@Override
- public Optional getMountPoint() {
- return Optional.ofNullable(mountPoint);
+ public VolumeImpl getImplementationType() {
+ return VolumeImpl.FUSE;
+ }
+
+ @Override
+ public MountPointRequirement getMountPointRequirement() {
+ return SystemUtils.IS_OS_WINDOWS ? MountPointRequirement.PARENT_NO_MOUNT_POINT : MountPointRequirement.EMPTY_MOUNT_POINT;
}
public static boolean isSupportedStatic() {
- return (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_LINUX) && FuseMountFactory.isFuseSupported();
+ return FuseMountFactory.isFuseSupported();
}
}
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/MountPointRequirement.java b/main/commons/src/main/java/org/cryptomator/common/vaults/MountPointRequirement.java
new file mode 100644
index 000000000..84a798e59
--- /dev/null
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/MountPointRequirement.java
@@ -0,0 +1,28 @@
+package org.cryptomator.common.vaults;
+
+/**
+ * Enumeration used to indicate the requirements for mounting a vault
+ * using a specific {@link Volume VolumeProvider}, e.g. {@link FuseVolume}.
+ */
+public enum MountPointRequirement {
+
+ /**
+ * No Mountpoint on the local filesystem required. (e.g. WebDAV)
+ */
+ NONE,
+
+ /**
+ * A parent folder is required, but the actual Mountpoint must not exist.
+ */
+ PARENT_NO_MOUNT_POINT,
+
+ /**
+ * A parent folder is required, but the actual Mountpoint may exist.
+ */
+ PARENT_OPT_MOUNT_POINT,
+
+ /**
+ * The actual Mountpoint must exist and must be empty.
+ */
+ EMPTY_MOUNT_POINT;
+}
\ No newline at end of file
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/PerVault.java b/main/commons/src/main/java/org/cryptomator/common/vaults/PerVault.java
index dab895b37..692349b61 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/PerVault.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/PerVault.java
@@ -8,6 +8,6 @@ import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
-@interface PerVault {
+public @interface PerVault {
}
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java
index 8a3735b65..f2ab973f6 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java
@@ -9,14 +9,11 @@
package org.cryptomator.common.vaults;
import com.google.common.base.Strings;
-import javafx.beans.Observable;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.binding.StringBinding;
-import javafx.beans.property.ObjectProperty;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.LazyInitializer;
+import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.settings.VaultSettings;
+import org.cryptomator.common.vaults.Volume.VolumeException;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
@@ -31,13 +28,20 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
+import javafx.beans.Observable;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.binding.StringBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
-import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
@@ -66,6 +70,7 @@ public class Vault {
private final BooleanBinding unknownError;
private final StringBinding accessPoint;
private final BooleanBinding accessPointPresent;
+ private final BooleanProperty showingStats;
private volatile Volume volume;
@@ -88,6 +93,7 @@ public class Vault {
this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
this.accessPoint = Bindings.createStringBinding(this::getAccessPoint, state);
this.accessPointPresent = this.accessPoint.isNotEmpty();
+ this.showingStats = new SimpleBooleanProperty(false);
}
// ******************************************************************************
@@ -120,16 +126,13 @@ public class Vault {
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
}
- public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
- if (vaultSettings.useCustomMountPath().get() && Strings.isNullOrEmpty(vaultSettings.customMountPath().get())) {
- throw new NotDirectoryException("");
- }
+ public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
CryptoFileSystem fs = getCryptoFileSystem(passphrase);
volume = volumeProvider.get();
volume.mount(fs, getEffectiveMountFlags());
}
- public synchronized void lock(boolean forced) throws Volume.VolumeException {
+ public synchronized void lock(boolean forced) throws VolumeException {
if (forced && volume.supportsForcedUnmount()) {
volume.unmountForced();
} else {
@@ -145,7 +148,7 @@ public class Vault {
}
}
- public void reveal() throws Volume.VolumeException {
+ public void reveal() throws VolumeException {
volume.reveal();
}
@@ -269,6 +272,15 @@ public class Vault {
}
}
+ public BooleanProperty showingStatsProperty() {
+ return showingStats;
+ }
+
+ public boolean isShowingStats() {
+ return accessPointPresent.get();
+ }
+
+
// ******************************************************************************
// Getter/Setter
// *******************************************************************************/
@@ -318,6 +330,10 @@ public class Vault {
return vaultSettings.getId();
}
+ public Optional getVolume() {
+ return Optional.ofNullable(this.volume);
+ }
+
// ******************************************************************************
// Hashcode / Equals
// *******************************************************************************/
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java
index 98a9f951d..debfab3ed 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java
@@ -6,15 +6,15 @@
package org.cryptomator.common.vaults;
import dagger.BindsInstance;
-import org.cryptomator.common.settings.VaultSettings;
-
import dagger.Subcomponent;
+import org.cryptomator.common.mountpoint.MountPointChooserModule;
+import org.cryptomator.common.settings.VaultSettings;
import javax.annotation.Nullable;
import javax.inject.Named;
@PerVault
-@Subcomponent(modules = {VaultModule.class})
+@Subcomponent(modules = {VaultModule.class, MountPointChooserModule.class})
public interface VaultComponent {
Vault vault();
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListChangeListener.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListChangeListener.java
index 147d6cc45..a7e422115 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListChangeListener.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListChangeListener.java
@@ -1,9 +1,9 @@
package org.cryptomator.common.vaults;
+import org.cryptomator.common.settings.VaultSettings;
+
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
-import org.cryptomator.common.settings.VaultSettings;
-
import java.util.List;
import java.util.stream.Collectors;
@@ -20,7 +20,7 @@ class VaultListChangeListener implements ListChangeListener {
@Override
public void onChanged(Change extends Vault> c) {
- while(c.next()) {
+ while (c.next()) {
if (c.wasAdded()) {
List addedSettings = c.getAddedSubList().stream().map(Vault::getVaultSettings).collect(Collectors.toList());
vaultSettingsList.addAll(c.getFrom(), addedSettings);
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java
index a670acab5..984008ba9 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java
@@ -8,8 +8,6 @@
*******************************************************************************/
package org.cryptomator.common.vaults;
-import javafx.collections.FXCollections;
-import javafx.collections.ObservableList;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
@@ -19,6 +17,8 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java
index 368bd516a..4844143a3 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java
@@ -7,12 +7,6 @@ package org.cryptomator.common.vaults;
import dagger.Module;
import dagger.Provides;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.StringBinding;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.ReadOnlyBooleanProperty;
-import javafx.beans.property.SimpleObjectProperty;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
@@ -23,6 +17,12 @@ import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.inject.Named;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.StringBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -84,6 +84,8 @@ public class VaultModule {
return getMacFuseDefaultMountFlags(mountName, readOnly);
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_LINUX) {
return getLinuxFuseDefaultMountFlags(readOnly);
+ } else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_WINDOWS) {
+ return getWindowsFuseDefaultMountFlags(mountName, readOnly);
} else if (v == VolumeImpl.DOKANY && SystemUtils.IS_OS_WINDOWS) {
return getDokanyDefaultMountFlags(readOnly);
} else {
@@ -142,6 +144,28 @@ public class VaultModule {
return flags.toString().strip();
}
+ // see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse_main.c#L53-L62 for syntax guide
+ // see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse.c#L295-L319 for options (-o <...>)
+ // see https://github.com/billziss-gh/winfsp/wiki/Frequently-Asked-Questions/5ba00e4be4f5e938eaae6ef1500b331de12dee77 (FUSE 4.) on why the given defaults were choosen
+ private String getWindowsFuseDefaultMountFlags(StringBinding mountName, ReadOnlyBooleanProperty readOnly) {
+ assert SystemUtils.IS_OS_WINDOWS;
+ StringBuilder flags = new StringBuilder();
+
+ //WinFSP has no explicit "readonly"-option, nut not setting the group/user-id has the same effect, tho.
+ //So for the time being not setting them is the way to go...
+ //See: https://github.com/billziss-gh/winfsp/issues/319
+ if (!readOnly.get()) {
+ flags.append(" -ouid=-1");
+ flags.append(" -ogid=-1");
+ }
+ flags.append(" -ovolname=").append(mountName.get());
+ //Dokany requires this option to be set, WinFSP doesn't seem to share this peculiarity,
+ //but the option exists. Let's keep this here in case we need it.
+// flags.append(" -oThreadCount=").append(5);
+
+ return flags.toString().strip();
+ }
+
// see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
private String getDokanyDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_WINDOWS;
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java
index a25ce59fa..fa5e6c295 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java
@@ -2,7 +2,7 @@ package org.cryptomator.common.vaults;
public enum VaultState {
/**
- * No vault found at the provided path
+ * No vault found at the provided path
*/
MISSING,
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java
index 69564b876..46cf31991 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java
@@ -1,19 +1,21 @@
package org.cryptomator.common.vaults;
-import javafx.application.Platform;
-import javafx.beans.Observable;
-import javafx.beans.property.LongProperty;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.SimpleLongProperty;
-import javafx.concurrent.ScheduledService;
-import javafx.concurrent.Task;
-import javafx.util.Duration;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemStats;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.LongProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleLongProperty;
+import javafx.concurrent.ScheduledService;
+import javafx.concurrent.Task;
+import javafx.util.Duration;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@@ -28,6 +30,15 @@ public class VaultStats {
private final ScheduledService> updateService;
private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
+ private final LongProperty bytesPerSecondEncrypted = new SimpleLongProperty();
+ private final LongProperty bytesPerSecondDecrypted = new SimpleLongProperty();
+ private final DoubleProperty cacheHitRate = new SimpleDoubleProperty();
+ private final LongProperty toalBytesRead = new SimpleLongProperty();
+ private final LongProperty toalBytesWritten = new SimpleLongProperty();
+ private final LongProperty totalBytesEncrypted = new SimpleLongProperty();
+ private final LongProperty totalBytesDecrypted = new SimpleLongProperty();
+ private final LongProperty filesRead = new SimpleLongProperty();
+ private final LongProperty filesWritten = new SimpleLongProperty();
@Inject
VaultStats(AtomicReference fs, ObjectProperty state, ExecutorService executor) {
@@ -53,12 +64,36 @@ public class VaultStats {
private void updateStats(Optional stats) {
assert Platform.isFxApplicationThread();
- bytesPerSecondRead.set(stats.map(CryptoFileSystemStats::pollBytesRead).orElse(0l));
- bytesPerSecondWritten.set(stats.map(CryptoFileSystemStats::pollBytesWritten).orElse(0l));
+ bytesPerSecondRead.set(stats.map(CryptoFileSystemStats::pollBytesRead).orElse(0L));
+ bytesPerSecondWritten.set(stats.map(CryptoFileSystemStats::pollBytesWritten).orElse(0L));
+ cacheHitRate.set(stats.map(this::getCacheHitRate).orElse(0.0));
+ bytesPerSecondDecrypted.set(stats.map(CryptoFileSystemStats::pollBytesDecrypted).orElse(0L));
+ bytesPerSecondEncrypted.set(stats.map(CryptoFileSystemStats::pollBytesEncrypted).orElse(0L));
+ toalBytesRead.set(stats.map(CryptoFileSystemStats::pollTotalBytesRead).orElse(0L));
+ toalBytesWritten.set(stats.map(CryptoFileSystemStats::pollTotalBytesWritten).orElse(0L));
+ totalBytesEncrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesEncrypted).orElse(0L));
+ totalBytesDecrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesDecrypted).orElse(0L));
+ filesRead.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesRead).orElse(0L));
+ filesWritten.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesWritten).orElse(0L));
+
+ }
+
+ private double getCacheHitRate(CryptoFileSystemStats stats) {
+ long accesses = stats.pollChunkCacheAccesses();
+ long hits = stats.pollChunkCacheHits();
+ if (accesses == 0) {
+ return 0.0;
+ } else {
+ return hits / (double) accesses;
+ }
}
private class UpdateStatsService extends ScheduledService> {
+ private UpdateStatsService() {
+ setOnFailed(event -> LOG.error("Error in UpdateStateService.", getException()));
+ }
+
@Override
protected Task> createTask() {
return new Task<>() {
@@ -94,4 +129,50 @@ public class VaultStats {
public long getBytesPerSecondWritten() {
return bytesPerSecondWritten.get();
}
+
+ public LongProperty bytesPerSecondEncryptedProperty() {
+ return bytesPerSecondEncrypted;
+ }
+
+ public long getBytesPerSecondEnrypted() {
+ return bytesPerSecondEncrypted.get();
+ }
+
+ public LongProperty bytesPerSecondDecryptedProperty() {
+ return bytesPerSecondDecrypted;
+ }
+
+ public long getBytesPerSecondDecrypted() {
+ return bytesPerSecondDecrypted.get();
+ }
+
+ public DoubleProperty cacheHitRateProperty() { return cacheHitRate; }
+
+ public double getCacheHitRate() {
+ return cacheHitRate.get();
+ }
+
+ public LongProperty toalBytesReadProperty() {return toalBytesRead;}
+
+ public long getTotalBytesRead() { return toalBytesRead.get();}
+
+ public LongProperty toalBytesWrittenProperty() {return toalBytesWritten;}
+
+ public long getTotalBytesWritten() { return toalBytesWritten.get();}
+
+ public LongProperty totalBytesEncryptedProperty() {return totalBytesEncrypted;}
+
+ public long getTotalBytesEncrypted() { return totalBytesEncrypted.get();}
+
+ public LongProperty totalBytesDecryptedProperty() {return totalBytesDecrypted;}
+
+ public long getTotalBytesDecrypted() { return totalBytesDecrypted.get();}
+
+ public LongProperty filesRead() { return filesRead;}
+
+ public long getFilesRead() { return filesRead.get();}
+
+ public LongProperty filesWritten() {return filesWritten;}
+
+ public long getFilesWritten() {return filesWritten.get();}
}
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java b/main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java
index a0aa11be8..624efea13 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java
@@ -1,5 +1,6 @@
package org.cryptomator.common.vaults;
+import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
@@ -20,11 +21,18 @@ public interface Volume {
*/
boolean isSupported();
+ /**
+ * Gets the coresponding enum type of the {@link VolumeImpl volume implementation ("VolumeImpl")} that is implemented by this Volume.
+ *
+ * @return the type of implementation as defined by the {@link VolumeImpl VolumeImpl enum}
+ */
+ VolumeImpl getImplementationType();
+
/**
* @param fs
* @throws IOException
*/
- void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException;
+ void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException, InvalidMountPointException;
void reveal() throws VolumeException;
@@ -32,6 +40,8 @@ public interface Volume {
Optional getMountPoint();
+ MountPointRequirement getMountPointRequirement();
+
// optional forced unmounting:
default boolean supportsForcedUnmount() {
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java b/main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
index 9c59790fa..fe9246085 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
@@ -1,8 +1,10 @@
package org.cryptomator.common.vaults;
+import com.google.common.base.CharMatcher;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
+import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.frontend.webdav.mount.MountParams;
@@ -44,7 +46,9 @@ public class WebDavVolume implements Volume {
if (!server.isRunning()) {
server.start();
}
- servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + vaultSettings.mountName().get());
+ CharMatcher acceptable = CharMatcher.inRange('0', '9').or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.inRange('a', 'z'));
+ String urlConformMountName = acceptable.negate().collapseFrom(vaultSettings.mountName().get(), '_');
+ servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + urlConformMountName);
servlet.start();
mount();
}
@@ -101,6 +105,11 @@ public class WebDavVolume implements Volume {
return Optional.ofNullable(mountPoint); //TODO
}
+ @Override
+ public MountPointRequirement getMountPointRequirement() {
+ return MountPointRequirement.NONE;
+ }
+
private String getLocalhostAliasOrNull() {
try {
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);
@@ -126,6 +135,11 @@ public class WebDavVolume implements Volume {
return WebDavVolume.isSupportedStatic();
}
+ @Override
+ public VolumeImpl getImplementationType() {
+ return VolumeImpl.WEBDAV;
+ }
+
@Override
public boolean supportsForcedUnmount() {
return mount != null && mount.forced().isPresent();
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java b/main/commons/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java
index 016c39a3a..f3ef2c7dc 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java
@@ -5,6 +5,7 @@
*******************************************************************************/
package org.cryptomator.common.vaults;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.SystemUtils;
@@ -12,6 +13,7 @@ import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.FileSystems;
import java.nio.file.Path;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -24,7 +26,7 @@ public final class WindowsDriveLetters {
static {
try (IntStream stream = IntStream.rangeClosed('C', 'Z')) {
- C_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(Collectors.toSet());
+ C_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(ImmutableSet.toImmutableSet());
}
}
@@ -46,7 +48,18 @@ public final class WindowsDriveLetters {
}
public Set getAvailableDriveLetters() {
- return Sets.difference(C_TO_Z, getOccupiedDriveLetters());
+ return Sets.difference(getAllDriveLetters(), getOccupiedDriveLetters());
}
+ public Optional getAvailableDriveLetter() {
+ return getAvailableDriveLetters().stream().findFirst();
+ }
+
+ public Optional getAvailableDriveLetterPath() {
+ return getAvailableDriveLetter().map(this::toPath);
+ }
+
+ public Path toPath(String driveLetter) {
+ return Path.of(driveLetter + ":\\");
+ }
}
diff --git a/main/commons/src/test/java/org/cryptomator/common/EnvironmentTest.java b/main/commons/src/test/java/org/cryptomator/common/EnvironmentTest.java
index cd8514397..3d3464191 100644
--- a/main/commons/src/test/java/org/cryptomator/common/EnvironmentTest.java
+++ b/main/commons/src/test/java/org/cryptomator/common/EnvironmentTest.java
@@ -37,7 +37,7 @@ class EnvironmentTest {
List result = env.getSettingsPath().collect(Collectors.toList());
MatcherAssert.assertThat(result, Matchers.hasSize(2));
- MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/settings.json"),
+ MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/settings.json"), //
Paths.get("/home/testuser/.Cryptomator/settings.json")));
}
@@ -48,7 +48,7 @@ class EnvironmentTest {
List result = env.getIpcPortPath().collect(Collectors.toList());
MatcherAssert.assertThat(result, Matchers.hasSize(2));
- MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/ipcPort.bin"),
+ MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/ipcPort.bin"), //
Paths.get("/home/testuser/.Cryptomator/ipcPort.bin")));
}
@@ -123,8 +123,8 @@ class EnvironmentTest {
List result = env.getPaths("test.path.property").collect(Collectors.toList());
MatcherAssert.assertThat(result, Matchers.hasSize(3));
- MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/test"),
- Paths.get("/home/testuser/test2"),
+ MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/test"), //
+ Paths.get("/home/testuser/test2"), //
Paths.get("/foo/bar/test")));
}
diff --git a/main/commons/src/test/java/org/cryptomator/common/LicenseCheckerTest.java b/main/commons/src/test/java/org/cryptomator/common/LicenseCheckerTest.java
index 5ae8f0fb9..6bb4befb4 100644
--- a/main/commons/src/test/java/org/cryptomator/common/LicenseCheckerTest.java
+++ b/main/commons/src/test/java/org/cryptomator/common/LicenseCheckerTest.java
@@ -1,7 +1,6 @@
package org.cryptomator.common;
import com.auth0.jwt.interfaces.DecodedJWT;
-import org.cryptomator.common.LicenseChecker;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -14,7 +13,7 @@ class LicenseCheckerTest {
+ "PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47" //
+ "6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM" //
+ "Al8G7CqwoJOsW7Kddns=";
-
+
private LicenseChecker licenseChecker;
@BeforeEach
@@ -25,9 +24,9 @@ class LicenseCheckerTest {
@Test
public void testCheckValidLicense() {
String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
-
+
Optional decoded = licenseChecker.check(license);
-
+
Assertions.assertTrue(decoded.isPresent());
Assertions.assertEquals("cryptobot@example.com", decoded.get().getSubject());
}
diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainManagerTest.java b/main/commons/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
similarity index 75%
rename from main/keychain/src/test/java/org/cryptomator/keychain/KeychainManagerTest.java
rename to main/commons/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
index 67f10e035..e82e67e2d 100644
--- a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainManagerTest.java
+++ b/main/commons/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
@@ -1,27 +1,30 @@
-package org.cryptomator.keychain;
+package org.cryptomator.common.keychain;
-import javafx.application.Platform;
-import javafx.beans.property.ReadOnlyBooleanProperty;
+import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import javafx.application.Platform;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
-class KeychainManagerTest {
-
+public class KeychainManagerTest {
+
@Test
public void testStoreAndLoad() throws KeychainAccessException {
- KeychainManager keychainManager = new KeychainManager(new MapKeychainAccess());
+ KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess()));
keychainManager.storePassphrase("test", "asd");
Assertions.assertArrayEquals("asd".toCharArray(), keychainManager.loadPassphrase("test"));
}
-
+
@Nested
public static class WhenObservingProperties {
@@ -31,15 +34,15 @@ class KeychainManagerTest {
Platform.startup(latch::countDown);
latch.await(5, TimeUnit.SECONDS);
}
-
+
@Test
public void testPropertyChangesWhenStoringPassword() throws KeychainAccessException, InterruptedException {
- KeychainManager keychainManager = new KeychainManager(new MapKeychainAccess());
+ KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess()));
ReadOnlyBooleanProperty property = keychainManager.getPassphraseStoredProperty("test");
Assertions.assertEquals(false, property.get());
-
+
keychainManager.storePassphrase("test", "bar");
-
+
AtomicBoolean result = new AtomicBoolean(false);
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
@@ -49,7 +52,7 @@ class KeychainManagerTest {
latch.await(1, TimeUnit.SECONDS);
Assertions.assertEquals(true, result.get());
}
-
+
}
}
\ No newline at end of file
diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java b/main/commons/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java
similarity index 86%
rename from main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java
rename to main/commons/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java
index 26b301377..c571ad716 100644
--- a/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java
+++ b/main/commons/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java
@@ -3,12 +3,14 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
-package org.cryptomator.keychain;
+package org.cryptomator.common.keychain;
+
+import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import java.util.HashMap;
import java.util.Map;
-class MapKeychainAccess implements KeychainAccessStrategy {
+class MapKeychainAccess implements KeychainAccessProvider {
private final Map map = new HashMap<>();
diff --git a/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java
index 347ec1e45..cee725332 100644
--- a/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java
+++ b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java
@@ -37,10 +37,10 @@ public class SettingsJsonAdapterTest {
}
@ParameterizedTest(name = "fromJson() should throw IOException for input: {0}")
- @ValueSource(strings = {
- "",
- "",
- "{invalidjson}"
+ @ValueSource(strings = { //
+ "", //
+ "", //
+ "{invalidjson}" //
})
public void testDeserializeMalformed(String input) {
Assertions.assertThrows(IOException.class, () -> {
diff --git a/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java
index a0faee551..ff33605af 100644
--- a/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java
+++ b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java
@@ -50,7 +50,7 @@ public class VaultSettingsJsonAdapterTest {
String result = buf.toString();
MatcherAssert.assertThat(result, CoreMatchers.containsString("\"id\":\"test\""));
- if(System.getProperty("os.name").contains("Windows")){
+ if (System.getProperty("os.name").contains("Windows")) {
MatcherAssert.assertThat(result, CoreMatchers.containsString("\"path\":\"\\\\foo\\\\bar\""));
} else {
MatcherAssert.assertThat(result, CoreMatchers.containsString("\"path\":\"/foo/bar\""));
diff --git a/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsTest.java
index 8ec6dc681..198e2937c 100644
--- a/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsTest.java
+++ b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsTest.java
@@ -16,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class VaultSettingsTest {
@ParameterizedTest
- @CsvSource({"a a,a_a", "ä,a", "Ĉ,C", ":,_", "汉语,_"})
+ @CsvSource({"a\u000Fa,a_a", ": \\,_ _", "汉语,汉语", "..,_", "a\ta,a\u0020a", "\t\n\r,_"})
public void testNormalize(String test, String expected) {
VaultSettings settings = new VaultSettings("id");
settings.displayName().setValue(test);
diff --git a/main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java b/main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java
index e4922963b..9b25ebb08 100644
--- a/main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java
+++ b/main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java
@@ -1,9 +1,5 @@
package org.cryptomator.common.vaults;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.StringBinding;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.property.SimpleObjectProperty;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
@@ -17,6 +13,11 @@ 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;
public class VaultModuleTest {
@@ -30,6 +31,7 @@ public class VaultModuleTest {
public void setup(@TempDir Path tmpDir) {
Mockito.when(vaultSettings.mountName()).thenReturn(Bindings.createStringBinding(() -> "TEST"));
Mockito.when(vaultSettings.usesReadOnlyMode()).thenReturn(new SimpleBooleanProperty(true));
+ Mockito.when(vaultSettings.displayName()).thenReturn(new SimpleStringProperty("Vault"));
System.setProperty("user.home", tmpDir.toString());
}
diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml
deleted file mode 100644
index d69d5ba83..000000000
--- a/main/keychain/pom.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
- 4.0.0
-
- org.cryptomator
- main
- 1.5.8
-
- keychain
- System Keychain Access
-
-
-
- org.cryptomator
- commons
-
-
-
-
- org.openjfx
- javafx-base
-
-
- org.openjfx
- javafx-graphics
-
-
-
-
- org.apache.commons
- commons-lang3
-
-
-
-
- com.google.code.gson
- gson
-
-
- com.google.guava
- guava
-
-
-
-
- com.google.dagger
- dagger
-
-
-
-
- de.swiesend
- secret-service
-
-
-
-
- org.slf4j
- slf4j-simple
- test
-
-
-
\ No newline at end of file
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessException.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessException.java
deleted file mode 100644
index 430b17b29..000000000
--- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessException.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.cryptomator.keychain;
-
-/**
- * Indicates an error during communication with the operating system's keychain.
- */
-public class KeychainAccessException extends Exception {
-
- KeychainAccessException(Throwable cause) {
- super(cause);
- }
-
-}
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java
deleted file mode 100644
index abd50287e..000000000
--- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.keychain;
-
-interface KeychainAccessStrategy {
-
- /**
- * Associates a passphrase with a given key.
- *
- * @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}.
- * @param passphrase The secret to store in this keychain.
- */
- void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException;
-
- /**
- * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
- * @return The stored passphrase for the given key or null if no value for the given key could be found.
- */
- char[] loadPassphrase(String key) throws KeychainAccessException;
-
- /**
- * Deletes a passphrase with a given key.
- *
- * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
- */
- void deletePassphrase(String key) throws KeychainAccessException;
-
- /**
- * Updates a passphrase with a given key. Noop, if there is no item for the given key.
- *
- * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
- * @param passphrase The secret to be updated in this keychain.
- */
- void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException;
-
- /**
- * @return true if this KeychainAccessStrategy works on the current machine.
- * @implNote This method must not throw any exceptions and should fail fast
- * returning false if it can't determine availability of the checked strategy
- */
- boolean isSupported();
-
-}
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java
deleted file mode 100644
index 1db94fec6..000000000
--- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.keychain;
-
-import dagger.Binds;
-import dagger.Module;
-import dagger.Provides;
-import dagger.multibindings.IntoSet;
-import org.cryptomator.common.JniModule;
-
-import javax.inject.Singleton;
-import java.util.Optional;
-import java.util.Set;
-
-@Module(includes = {JniModule.class})
-public abstract class KeychainModule {
-
- @Binds
- @IntoSet
- abstract KeychainAccessStrategy bindMacSystemKeychainAccess(MacSystemKeychainAccess keychainAccessStrategy);
-
- @Binds
- @IntoSet
- abstract KeychainAccessStrategy bindWindowsProtectedKeychainAccess(WindowsProtectedKeychainAccess keychainAccessStrategy);
-
- @Binds
- @IntoSet
- abstract KeychainAccessStrategy bindLinuxSecretServiceKeychainAccess(LinuxSecretServiceKeychainAccess keychainAccessStrategy);
-
- @Provides
- @Singleton
- static Optional provideSupportedKeychain(Set keychainAccessStrategies) {
- return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).findFirst();
- }
-
- @Provides
- @Singleton
- public static Optional provideKeychainManager(Optional keychainAccess) {
- return keychainAccess.map(KeychainManager::new);
- }
-
-}
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccess.java
deleted file mode 100644
index f11bdbd45..000000000
--- a/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccess.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package org.cryptomator.keychain;
-
-import org.apache.commons.lang3.SystemUtils;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import java.util.Optional;
-
-/**
- * A facade to LinuxSecretServiceKeychainAccessImpl that doesn't depend on libraries that are unavailable on Mac and Windows.
- */
-@Singleton
-public class LinuxSecretServiceKeychainAccess implements KeychainAccessStrategy {
-
- // the actual implementation is hidden in this delegate object which is loaded via reflection,
- // as it depends on libraries that aren't necessarily available:
- private final Optional delegate;
-
- @Inject
- public LinuxSecretServiceKeychainAccess() {
- this.delegate = constructGnomeKeyringKeychainAccess();
- }
-
- private static Optional constructGnomeKeyringKeychainAccess() {
- try {
- Class> clazz = Class.forName("org.cryptomator.keychain.LinuxSecretServiceKeychainAccessImpl");
- KeychainAccessStrategy instance = (KeychainAccessStrategy) clazz.getDeclaredConstructor().newInstance();
- return Optional.of(instance);
- } catch (Exception e) {
- return Optional.empty();
- }
- }
-
- @Override
- public boolean isSupported() {
- return SystemUtils.IS_OS_LINUX && delegate.map(KeychainAccessStrategy::isSupported).orElse(false);
- }
-
- @Override
- public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
- delegate.orElseThrow(IllegalStateException::new).storePassphrase(key, passphrase);
- }
-
- @Override
- public char[] loadPassphrase(String key) throws KeychainAccessException {
- return delegate.orElseThrow(IllegalStateException::new).loadPassphrase(key);
- }
-
- @Override
- public void deletePassphrase(String key) throws KeychainAccessException {
- delegate.orElseThrow(IllegalStateException::new).deletePassphrase(key);
- }
-
- @Override
- public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
- delegate.orElseThrow(IllegalStateException::new).changePassphrase(key, passphrase);
- }
-}
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccessImpl.java b/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccessImpl.java
deleted file mode 100644
index 675992e5c..000000000
--- a/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccessImpl.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package org.cryptomator.keychain;
-
-import org.freedesktop.secret.simple.SimpleCollection;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {
-
- private final String LABEL_FOR_SECRET_IN_KEYRING = "Cryptomator";
-
- @Override
- public boolean isSupported() {
- try (@SuppressWarnings("unused") SimpleCollection keyring = new SimpleCollection()) {
- // seems like we're able to access the keyring.
- return true;
- } catch (IOException | RuntimeException e) {
- return false;
- }
- }
-
- @Override
- public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
- try (SimpleCollection keyring = new SimpleCollection()) {
- List list = keyring.getItems(createAttributes(key));
- if (list == null) {
- keyring.createItem(LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key));
- }
- } catch (IOException e) {
- throw new KeychainAccessException(e);
- }
- }
-
- @Override
- public char[] loadPassphrase(String key) throws KeychainAccessException {
- try (SimpleCollection keyring = new SimpleCollection()) {
- List list = keyring.getItems(createAttributes(key));
- if (list != null) {
- return keyring.getSecret(list.get(0));
- } else {
- return null;
- }
- } catch (IOException e) {
- throw new KeychainAccessException(e);
- }
- }
-
- @Override
- public void deletePassphrase(String key) throws KeychainAccessException {
- try (SimpleCollection keyring = new SimpleCollection()) {
- List list = keyring.getItems(createAttributes(key));
- if (list != null) {
- keyring.deleteItem(list.get(0));
- }
- } catch (IOException e) {
- throw new KeychainAccessException(e);
- }
- }
-
- @Override
- public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
- try (SimpleCollection keyring = new SimpleCollection()) {
- List list = keyring.getItems(createAttributes(key));
- if (list != null) {
- keyring.updateItem(list.get(0), LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key));
- }
- } catch (IOException e) {
- throw new KeychainAccessException(e);
- }
- }
-
- private Map createAttributes(String key) {
- Map attributes = new HashMap();
- attributes.put("Vault", key);
- return attributes;
- }
-}
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java
deleted file mode 100644
index 0021cc13d..000000000
--- a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.keychain;
-
-import java.util.Optional;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.jni.MacFunctions;
-import org.cryptomator.jni.MacKeychainAccess;
-
-@Singleton
-class MacSystemKeychainAccess implements KeychainAccessStrategy {
-
- private final Optional macFunctions;
-
- @Inject
- public MacSystemKeychainAccess(Optional macFunctions) {
- this.macFunctions = macFunctions;
- }
-
- private MacKeychainAccess keychain() {
- return macFunctions.orElseThrow(IllegalStateException::new).keychainAccess();
- }
-
- @Override
- public void storePassphrase(String key, CharSequence passphrase) {
- keychain().storePassword(key, passphrase);
- }
-
- @Override
- public char[] loadPassphrase(String key) {
- return keychain().loadPassword(key);
- }
-
- @Override
- public boolean isSupported() {
- return SystemUtils.IS_OS_MAC_OSX && macFunctions.isPresent();
- }
-
- @Override
- public void deletePassphrase(String key) {
- keychain().deletePassword(key);
- }
-
- @Override
- public void changePassphrase(String key, CharSequence passphrase) {
- if (keychain().deletePassword(key)) {
- keychain().storePassword(key, passphrase);
- }
- }
-
-}
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java
deleted file mode 100644
index 6668104a4..000000000
--- a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.keychain;
-
-import com.google.common.io.BaseEncoding;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonDeserializationContext;
-import com.google.gson.JsonDeserializer;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonPrimitive;
-import com.google.gson.JsonSerializationContext;
-import com.google.gson.JsonSerializer;
-import com.google.gson.annotations.SerializedName;
-import com.google.gson.reflect.TypeToken;
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.Environment;
-import org.cryptomator.jni.WinDataProtection;
-import org.cryptomator.jni.WinFunctions;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Reader;
-import java.io.UncheckedIOException;
-import java.io.Writer;
-import java.lang.reflect.Type;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.file.Files;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-import java.util.stream.Collectors;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-@Singleton
-class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
-
- private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class);
- private static final Gson GSON = new GsonBuilder().setPrettyPrinting() //
- .registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) //
- .disableHtmlEscaping().create();
-
- private final Optional winFunctions;
- private final List keychainPaths;
- private Map keychainEntries;
-
- @Inject
- public WindowsProtectedKeychainAccess(Optional winFunctions, Environment environment) {
- this.winFunctions = winFunctions;
- this.keychainPaths = environment.getKeychainPath().collect(Collectors.toList());
- }
-
- private WinDataProtection dataProtection() {
- return winFunctions.orElseThrow(IllegalStateException::new).dataProtection();
- }
-
- @Override
- public void storePassphrase(String key, CharSequence passphrase) {
- loadKeychainEntriesIfNeeded();
- ByteBuffer buf = UTF_8.encode(CharBuffer.wrap(passphrase));
- byte[] cleartext = new byte[buf.remaining()];
- buf.get(cleartext);
- KeychainEntry entry = new KeychainEntry();
- entry.salt = generateSalt();
- entry.ciphertext = dataProtection().protect(cleartext, entry.salt);
- Arrays.fill(buf.array(), (byte) 0x00);
- Arrays.fill(cleartext, (byte) 0x00);
- keychainEntries.put(key, entry);
- saveKeychainEntries();
- }
-
- @Override
- public char[] loadPassphrase(String key) {
- loadKeychainEntriesIfNeeded();
- KeychainEntry entry = keychainEntries.get(key);
- if (entry == null) {
- return null;
- }
- byte[] cleartext = dataProtection().unprotect(entry.ciphertext, entry.salt);
- if (cleartext == null) {
- return null;
- }
- CharBuffer buf = UTF_8.decode(ByteBuffer.wrap(cleartext));
- char[] passphrase = new char[buf.remaining()];
- buf.get(passphrase);
- Arrays.fill(cleartext, (byte) 0x00);
- Arrays.fill(buf.array(), (char) 0x00);
- return passphrase;
- }
-
- @Override
- public void deletePassphrase(String key) {
- loadKeychainEntriesIfNeeded();
- keychainEntries.remove(key);
- saveKeychainEntries();
- }
-
- @Override
- public void changePassphrase(String key, CharSequence passphrase) {
- loadKeychainEntriesIfNeeded();
- if (keychainEntries.remove(key) != null) {
- storePassphrase(key, passphrase);
- }
- }
-
- @Override
- public boolean isSupported() {
- return SystemUtils.IS_OS_WINDOWS && winFunctions.isPresent() && !keychainPaths.isEmpty();
- }
-
- private byte[] generateSalt() {
- byte[] result = new byte[2 * Long.BYTES];
- UUID uuid = UUID.randomUUID();
- ByteBuffer buf = ByteBuffer.wrap(result);
- buf.putLong(uuid.getMostSignificantBits());
- buf.putLong(uuid.getLeastSignificantBits());
- return result;
- }
-
- private void loadKeychainEntriesIfNeeded() {
- if (keychainEntries == null) {
- for (Path keychainPath : keychainPaths) {
- Optional