~exprez135/cryptomator-libre

41492a951ab2b1227edf590786a9490daac29884 — Sebastian Stenzel 10 months ago 8ad20c2 + cbbfec5 1.5.9
Merge branch 'release/1.5.9'
303 files changed, 5184 insertions(+), 2506 deletions(-)

M .github/ISSUE_TEMPLATE/bug.md
M .github/workflows/build.yml
A .github/workflows/triageBugs.yml
M .gitignore
M .idea/codeStyles/Project.xml
D .idea/compiler.xml
D .idea/encodings.xml
D .idea/jarRepositories.xml
A .idea/runConfigurations/Cryptomator_Linux_Dev.xml
M .idea/runConfigurations/Cryptomator_Windows.xml
A .idea/runConfigurations/Cryptomator_Windows_Dev.xml
A .idea/runConfigurations/Cryptomator_macOS_Dev.xml
M README.md
M main/buildkit/assembly-linux.xml
M main/buildkit/assembly-mac.xml
M main/buildkit/assembly-win.xml
M main/buildkit/pom.xml
D main/buildkit/src/main/resources/ffi-version.txt
M main/buildkit/src/main/resources/launcher-linux.sh
M main/buildkit/src/main/resources/launcher-mac.sh
M main/buildkit/src/main/resources/launcher-win.bat
M main/commons/pom.xml
M main/commons/src/main/java/org/cryptomator/common/CommonsModule.java
M main/commons/src/main/java/org/cryptomator/common/Environment.java
D main/commons/src/main/java/org/cryptomator/common/JniModule.java
M main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java
M main/commons/src/main/java/org/cryptomator/common/LicenseHolder.java
M main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java
R main/{keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java => commons/src/main/java/org/cryptomator/common/keychain/KeychainManager.java}
A main/commons/src/main/java/org/cryptomator/common/keychain/KeychainModule.java
A main/commons/src/main/java/org/cryptomator/common/keychain/NoKeychainAccessProviderException.java
A main/commons/src/main/java/org/cryptomator/common/mountpoint/AvailableDriveLetterChooser.java
A main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomDriveLetterChooser.java
A main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java
A main/commons/src/main/java/org/cryptomator/common/mountpoint/InvalidMountPointException.java
A main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java
A main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java
A main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java
A main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java
A main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java
M main/commons/src/main/java/org/cryptomator/common/settings/Settings.java
M main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
M main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java
M main/commons/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java
A main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java
M main/commons/src/main/java/org/cryptomator/common/vaults/DefaultMountFlags.java
M main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java
M main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java
A main/commons/src/main/java/org/cryptomator/common/vaults/MountPointRequirement.java
M main/commons/src/main/java/org/cryptomator/common/vaults/PerVault.java
M main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java
M main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java
M main/commons/src/main/java/org/cryptomator/common/vaults/VaultListChangeListener.java
M main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java
M main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java
M main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java
M main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java
M main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java
M main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
M main/commons/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java
M main/commons/src/test/java/org/cryptomator/common/EnvironmentTest.java
M main/commons/src/test/java/org/cryptomator/common/LicenseCheckerTest.java
R main/{keychain/src/test/java/org/cryptomator/keychain/KeychainManagerTest.java => commons/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java}
R main/{keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java => commons/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java}
M main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java
M main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java
M main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsTest.java
M main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java
D main/keychain/pom.xml
D main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessException.java
D main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java
D main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java
D main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccess.java
D main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccessImpl.java
D main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java
D main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java
D main/keychain/src/test/java/org/cryptomator/keychain/WindowsProtectedKeychainAccessTest.java
D main/keychain/src/test/resources/log4j2.xml
M main/launcher/pom.xml
M main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java
M main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java
M main/launcher/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java
M main/launcher/src/main/java/org/cryptomator/logging/DebugMode.java
M main/launcher/src/main/java/org/cryptomator/logging/LaunchBasedTriggeringPolicy.java
M main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java
M main/launcher/src/test/java/org/cryptomator/launcher/IpcFactoryTest.java
M main/pom.xml
M main/ui/pom.xml
M main/ui/src/license/template.ftl
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultWelcomeController.java
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultWizardComponent.java
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultNameController.java
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultRecoveryKeyController.java
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java
M main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordComponent.java
M main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java
M main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java
M main/ui/src/main/java/org/cryptomator/ui/common/Animations.java
M main/ui/src/main/java/org/cryptomator/ui/common/DefaultSceneFactory.java
M main/ui/src/main/java/org/cryptomator/ui/common/ErrorComponent.java
M main/ui/src/main/java/org/cryptomator/ui/common/ErrorController.java
M main/ui/src/main/java/org/cryptomator/ui/common/ErrorModule.java
M main/ui/src/main/java/org/cryptomator/ui/common/FXMLLoaderFactory.java
M main/ui/src/main/java/org/cryptomator/ui/common/FontLoader.java
M main/ui/src/main/java/org/cryptomator/ui/common/FxController.java
M main/ui/src/main/java/org/cryptomator/ui/common/FxControllerKey.java
M main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java
M main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java
M main/ui/src/main/java/org/cryptomator/ui/common/StageFactory.java
M main/ui/src/main/java/org/cryptomator/ui/common/Tasks.java
M main/ui/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java
M main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java
M main/ui/src/main/java/org/cryptomator/ui/common/WeakBindings.java
M main/ui/src/main/java/org/cryptomator/ui/controls/AlphanumericTextField.java
A main/ui/src/main/java/org/cryptomator/ui/controls/DataLabel.java
M main/ui/src/main/java/org/cryptomator/ui/controls/DraggableListCell.java
M main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
M main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java
M main/ui/src/main/java/org/cryptomator/ui/controls/FormattedString.java
M main/ui/src/main/java/org/cryptomator/ui/controls/NumericTextField.java
M main/ui/src/main/java/org/cryptomator/ui/controls/PasswordStrengthIndicator.java
M main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java
M main/ui/src/main/java/org/cryptomator/ui/controls/ThrougputLabel.java
M main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordComponent.java
M main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordController.java
M main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordModule.java
M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationComponent.java
M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
M main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java
M main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java
M main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerTask.java
M main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEvent.java
M main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java
M main/ui/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java
M main/ui/src/main/java/org/cryptomator/ui/launcher/FxApplicationStarter.java
M main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java
M main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncherModule.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowComponent.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowSceneFactory.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailMissingVaultController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailNeedsMigrationController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnknownErrorController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellFactory.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/WelcomeController.java
M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationCapabilityErrorController.java
M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationComponent.java
M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationImpossibleController.java
M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java
M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java
M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationStartController.java
M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesComponent.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java
M main/ui/src/main/java/org/cryptomator/ui/quit/QuitComponent.java
M main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java
M main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java
M main/ui/src/main/java/org/cryptomator/ui/recoverykey/AutoCompleter.java
M main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java
M main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java
M main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyDisplayController.java
M main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java
M main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java
M main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java
M main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java
M main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeySuccessController.java
M main/ui/src/main/java/org/cryptomator/ui/recoverykey/WordEncoder.java
M main/ui/src/main/java/org/cryptomator/ui/removevault/RemoveVaultComponent.java
M main/ui/src/main/java/org/cryptomator/ui/removevault/RemoveVaultController.java
M main/ui/src/main/java/org/cryptomator/ui/removevault/RemoveVaultModule.java
A main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsComponent.java
A main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsController.java
A main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsModule.java
A main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsScoped.java
A main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsWindow.java
M main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayIconController.java
M main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java
M main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java
M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java
M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsComponent.java
M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java
M main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertComponent.java
M main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertController.java
M main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java
M main/ui/src/main/resources/css/dark_theme.css
M main/ui/src/main/resources/css/light_theme.css
M main/ui/src/main/resources/fxml/addvault_existing.fxml
M main/ui/src/main/resources/fxml/addvault_new_location.fxml
M main/ui/src/main/resources/fxml/addvault_new_name.fxml
M main/ui/src/main/resources/fxml/addvault_new_password.fxml
M main/ui/src/main/resources/fxml/addvault_new_recoverykey.fxml
M main/ui/src/main/resources/fxml/addvault_success.fxml
M main/ui/src/main/resources/fxml/addvault_welcome.fxml
M main/ui/src/main/resources/fxml/changepassword.fxml
M main/ui/src/main/resources/fxml/error.fxml
M main/ui/src/main/resources/fxml/forget_password.fxml
M main/ui/src/main/resources/fxml/main_window.fxml
M main/ui/src/main/resources/fxml/main_window_resize.fxml
M main/ui/src/main/resources/fxml/main_window_title.fxml
M main/ui/src/main/resources/fxml/migration_capability_error.fxml
M main/ui/src/main/resources/fxml/migration_impossible.fxml
M main/ui/src/main/resources/fxml/migration_run.fxml
M main/ui/src/main/resources/fxml/migration_start.fxml
M main/ui/src/main/resources/fxml/migration_success.fxml
M main/ui/src/main/resources/fxml/new_password.fxml
M main/ui/src/main/resources/fxml/preferences.fxml
M main/ui/src/main/resources/fxml/preferences_about.fxml
M main/ui/src/main/resources/fxml/preferences_donationkey.fxml
M main/ui/src/main/resources/fxml/preferences_general.fxml
M main/ui/src/main/resources/fxml/preferences_updates.fxml
M main/ui/src/main/resources/fxml/preferences_volume.fxml
M main/ui/src/main/resources/fxml/quit.fxml
M main/ui/src/main/resources/fxml/recoverykey_create.fxml
M main/ui/src/main/resources/fxml/recoverykey_display.fxml
M main/ui/src/main/resources/fxml/recoverykey_recover.fxml
M main/ui/src/main/resources/fxml/recoverykey_reset_password.fxml
M main/ui/src/main/resources/fxml/recoverykey_success.fxml
M main/ui/src/main/resources/fxml/remove_vault.fxml
A main/ui/src/main/resources/fxml/stats.fxml
M main/ui/src/main/resources/fxml/unlock.fxml
M main/ui/src/main/resources/fxml/unlock_invalid_mount_point.fxml
M main/ui/src/main/resources/fxml/unlock_success.fxml
M main/ui/src/main/resources/fxml/vault_detail.fxml
M main/ui/src/main/resources/fxml/vault_detail_locked.fxml
M main/ui/src/main/resources/fxml/vault_detail_missing.fxml
M main/ui/src/main/resources/fxml/vault_detail_needsmigration.fxml
M main/ui/src/main/resources/fxml/vault_detail_unknownerror.fxml
M main/ui/src/main/resources/fxml/vault_detail_unlocked.fxml
M main/ui/src/main/resources/fxml/vault_detail_welcome.fxml
M main/ui/src/main/resources/fxml/vault_list.fxml
M main/ui/src/main/resources/fxml/vault_list_cell.fxml
M main/ui/src/main/resources/fxml/vault_options.fxml
M main/ui/src/main/resources/fxml/vault_options_general.fxml
M main/ui/src/main/resources/fxml/vault_options_masterkey.fxml
M main/ui/src/main/resources/fxml/vault_options_mount.fxml
M main/ui/src/main/resources/fxml/wrongfilealert.fxml
M main/ui/src/main/resources/i18n/strings.properties
M main/ui/src/main/resources/i18n/strings_ar.properties
A main/ui/src/main/resources/i18n/strings_bs.properties
M main/ui/src/main/resources/i18n/strings_ca.properties
M main/ui/src/main/resources/i18n/strings_cs.properties
M main/ui/src/main/resources/i18n/strings_de.properties
M main/ui/src/main/resources/i18n/strings_el.properties
M main/ui/src/main/resources/i18n/strings_es.properties
M main/ui/src/main/resources/i18n/strings_fr.properties
M main/ui/src/main/resources/i18n/strings_hi.properties
M main/ui/src/main/resources/i18n/strings_hr.properties
A main/ui/src/main/resources/i18n/strings_id.properties
M main/ui/src/main/resources/i18n/strings_it.properties
M main/ui/src/main/resources/i18n/strings_ja.properties
M main/ui/src/main/resources/i18n/strings_ko.properties
M main/ui/src/main/resources/i18n/strings_lv.properties
M main/ui/src/main/resources/i18n/strings_nb.properties
M main/ui/src/main/resources/i18n/strings_nl.properties
M main/ui/src/main/resources/i18n/strings_nn.properties
A main/ui/src/main/resources/i18n/strings_pa.properties
M main/ui/src/main/resources/i18n/strings_pl.properties
M main/ui/src/main/resources/i18n/strings_pt.properties
M main/ui/src/main/resources/i18n/strings_pt_BR.properties
M main/ui/src/main/resources/i18n/strings_ro.properties
M main/ui/src/main/resources/i18n/strings_ru.properties
M main/ui/src/main/resources/i18n/strings_sk.properties
M main/ui/src/main/resources/i18n/strings_sv.properties
M main/ui/src/main/resources/i18n/strings_tr.properties
M main/ui/src/main/resources/i18n/strings_zh.properties
M main/ui/src/main/resources/i18n/strings_zh_TW.properties
M main/ui/src/main/resources/license/THIRD-PARTY.txt
M main/ui/src/test/java/org/cryptomator/ui/common/PasswordStrengthUtilTest.java
M main/ui/src/test/java/org/cryptomator/ui/controls/SecurePasswordFieldTest.java
M main/ui/src/test/java/org/cryptomator/ui/recoverykey/AutoCompleterTest.java
M main/ui/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java
M .github/ISSUE_TEMPLATE/bug.md => .github/ISSUE_TEMPLATE/bug.md +3 -0
@@ 5,6 5,7 @@ labels: type:bug
---

<!--
⚠️⚠️⚠️ READ CAREFULLY ⚠️⚠️⚠️

Do you want to ask a QUESTION? Are you looking for SUPPORT?
We're happy to help you via our support channels! Please read: https://github.com/cryptomator/cryptomator/blob/develop/SUPPORT.md


@@ 13,7 14,9 @@ By filing an issue, you are expected to comply with our code of conduct: https:/

Of course, we also expect you to search for existing similar issues first! ;) https://github.com/cryptomator/cryptomator/issues?q=

⚠️ IMPORTANT: If you don't stick to this template, the issue will get closed. To proof that you read this, please remove the X from the following line:
-->
<!-- oooXooo -->

### Description


M .github/workflows/build.yml => .github/workflows/build.yml +7 -3
@@ 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:

A .github/workflows/triageBugs.yml => .github/workflows/triageBugs.yml +25 -0
@@ 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, '<!-- oooooo -->')
            || !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

M .gitignore => .gitignore +3 -0
@@ 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

M .idea/codeStyles/Project.xml => .idea/codeStyles/Project.xml +28 -1
@@ 9,14 9,36 @@
    <option name="RIGHT_MARGIN" value="220" />
    <option name="FORMATTER_TAGS_ENABLED" value="true" />
    <JavaCodeStyleSettings>
      <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="30" />
      <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
      <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="10" />
      <option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
        <value />
      </option>
      <option name="IMPORT_LAYOUT_TABLE">
        <value>
          <package name="" withSubpackages="true" static="false" />
          <emptyLine />
          <package name="javax" withSubpackages="true" static="false" />
          <package name="javafx" withSubpackages="true" static="false" />
          <package name="java" withSubpackages="true" static="false" />
          <emptyLine />
          <package name="" withSubpackages="true" static="true" />
        </value>
      </option>
      <option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
      <option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
    </JavaCodeStyleSettings>
    <Properties>
      <option name="KEEP_BLANK_LINES" value="true" />
    </Properties>
    <XML>
      <option name="XML_ATTRIBUTE_WRAP" value="0" />
    </XML>
    <codeStyleSettings language="CSS">
      <indentOptions>
        <option name="USE_TAB_CHARACTER" value="true" />
      </indentOptions>
    </codeStyleSettings>
    <codeStyleSettings language="Groovy">
      <indentOptions>
        <option name="USE_TAB_CHARACTER" value="true" />


@@ 30,12 52,17 @@
    <codeStyleSettings language="JAVA">
      <option name="KEEP_LINE_BREAKS" value="false" />
      <option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
      <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
      <option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
      <option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
      <option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
      <option name="ENUM_CONSTANTS_WRAP" value="2" />
      <indentOptions>
        <option name="USE_TAB_CHARACTER" value="true" />
      </indentOptions>
      <arrangement>
        <rules />
      </arrangement>
    </codeStyleSettings>
    <codeStyleSettings language="JSON">
      <indentOptions>

D .idea/compiler.xml => .idea/compiler.xml +0 -33
@@ 1,33 0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="CompilerConfiguration">
    <annotationProcessing>
      <profile name="Annotation profile for Cryptomator" enabled="true">
        <sourceOutputDir name="target/generated-sources/annotations" />
        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
        <outputRelativeToContentRoot value="true" />
        <processorPath useClasspath="false">
          <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.22/dagger-compiler-2.22.jar" />
          <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.22/dagger-2.22.jar" />
          <entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
          <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.22/dagger-producers-2.22.jar" />
          <entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/25.0-jre/guava-25.0-jre.jar" />
          <entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar" />
          <entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" />
          <entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.1.3/error_prone_annotations-2.1.3.jar" />
          <entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar" />
          <entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.14/animal-sniffer-annotations-1.14.jar" />
          <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.22/dagger-spi-2.22.jar" />
          <entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar" />
          <entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
          <entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
          <entry name="$MAVEN_REPOSITORY$/javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar" />
        </processorPath>
        <module name="keychain" />
        <module name="commons" />
        <module name="ui" />
        <module name="launcher" />
      </profile>
    </annotationProcessing>
  </component>
</project>
\ No newline at end of file

D .idea/encodings.xml => .idea/encodings.xml +0 -13
@@ 1,13 0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
    <file url="file://$PROJECT_DIR$/main" charset="UTF-8" />
    <file url="file://$PROJECT_DIR$/main/commons/src/main/java" charset="UTF-8" />
    <file url="file://$PROJECT_DIR$/main/keychain/src/main/java" charset="UTF-8" />
    <file url="file://$PROJECT_DIR$/main/keychain/src/main/resources" charset="UTF-8" />
    <file url="file://$PROJECT_DIR$/main/launcher/src/main/java" charset="UTF-8" />
    <file url="file://$PROJECT_DIR$/main/ui/src/main/java" charset="UTF-8" />
    <file url="file://$PROJECT_DIR$/main/ui/src/main/resources" charset="UTF-8" />
    <file url="PROJECT" charset="UTF-8" />
  </component>
</project>
\ No newline at end of file

D .idea/jarRepositories.xml => .idea/jarRepositories.xml +0 -30
@@ 1,30 0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="RemoteRepositoriesConfiguration">
    <remote-repository>
      <option name="id" value="bintray" />
      <option name="name" value="Bintray JCenter" />
      <option name="url" value="https://jcenter.bintray.com" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="central" />
      <option name="name" value="Maven Central Repository" />
      <option name="url" value="https://repo.maven.apache.org/maven2" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="central" />
      <option name="name" value="Maven Central repository" />
      <option name="url" value="https://repo1.maven.org/maven2" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="jcenter" />
      <option name="name" value="jcenter" />
      <option name="url" value="https://jcenter.bintray.com" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="jboss.community" />
      <option name="name" value="JBoss Community repository" />
      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
    </remote-repository>
  </component>
</project>
\ No newline at end of file

A .idea/runConfigurations/Cryptomator_Linux_Dev.xml => .idea/runConfigurations/Cryptomator_Linux_Dev.xml +10 -0
@@ 0,0 1,10 @@
<component name="ProjectRunConfigurationManager">
  <configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
    <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
    <module name="launcher" />
    <option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/.config/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m" />
    <method v="2">
      <option name="Make" enabled="true" />
    </method>
  </configuration>
</component>
\ No newline at end of file

M .idea/runConfigurations/Cryptomator_Windows.xml => .idea/runConfigurations/Cryptomator_Windows.xml +1 -1
@@ 2,7 2,7 @@
  <configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
    <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
    <module name="launcher" />
    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Xss2m -Xmx512m" />
    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Xss2m -Xmx512m" />
    <method v="2">
      <option name="Make" enabled="true" />
    </method>

A .idea/runConfigurations/Cryptomator_Windows_Dev.xml => .idea/runConfigurations/Cryptomator_Windows_Dev.xml +10 -0
@@ 0,0 1,10 @@
<component name="ProjectRunConfigurationManager">
  <configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
    <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
    <module name="launcher" />
    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator-Dev&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator-Dev&quot; -Dfuse.experimental=&quot;true&quot; -Xss2m -Xmx512m" />
    <method v="2">
      <option name="Make" enabled="true" />
    </method>
  </configuration>
</component>
\ No newline at end of file

A .idea/runConfigurations/Cryptomator_macOS_Dev.xml => .idea/runConfigurations/Cryptomator_macOS_Dev.xml +13 -0
@@ 0,0 1,13 @@
<component name="ProjectRunConfigurationManager">
  <configuration default="false" name="Cryptomator macOS Dev" type="Application" factoryName="Application">
    <envs>
      <env name="LD_LIBRARY_PATH" value="/usr/local/lib" />
    </envs>
    <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
    <module name="launcher" />
    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.mountPointsDir=&quot;/Volumes/&quot; -Dfuse.experimental=&quot;true&quot; -Xss2m -Xmx512m -ea" />
    <method v="2">
      <option name="Make" enabled="true" />
    </method>
  </configuration>
</component>
\ No newline at end of file

M README.md => README.md +3 -1
@@ 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.

M main/buildkit/assembly-linux.xml => main/buildkit/assembly-linux.xml +1 -15
@@ 1,5 1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
		  xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
	<id>tarball</id>
	<includeBaseDirectory>false</includeBaseDirectory>


@@ 17,13 17,6 @@
		<fileSet>
			<directory>target/</directory>
			<includes>
				<include>ffi-version.txt</include>
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
		<fileSet>
			<directory>target/</directory>
			<includes>
				<include>LICENSE.txt</include>
			</includes>
			<outputDirectory></outputDirectory>


@@ 43,12 36,5 @@
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
		<fileSet>
			<directory>target/linux-libs</directory>
			<includes>
				<include>*.jar</include>
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
	</fileSets>
</assembly>
\ No newline at end of file

M main/buildkit/assembly-mac.xml => main/buildkit/assembly-mac.xml +1 -15
@@ 1,5 1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
		  xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
	<id>tarball</id>
	<includeBaseDirectory>false</includeBaseDirectory>


@@ 17,13 17,6 @@
		<fileSet>
			<directory>target/</directory>
			<includes>
				<include>ffi-version.txt</include>
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
		<fileSet>
			<directory>target/</directory>
			<includes>
				<include>LICENSE.txt</include>
			</includes>
			<outputDirectory></outputDirectory>


@@ 43,12 36,5 @@
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
		<fileSet>
			<directory>target/mac-libs</directory>
			<includes>
				<include>*.jar</include>
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
	</fileSets>
</assembly>
\ No newline at end of file

M main/buildkit/assembly-win.xml => main/buildkit/assembly-win.xml +1 -15
@@ 1,5 1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
		  xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
	<id>tarball</id>
	<includeBaseDirectory>false</includeBaseDirectory>


@@ 17,13 17,6 @@
		<fileSet>
			<directory>target/</directory>
			<includes>
				<include>ffi-version.txt</include>
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
		<fileSet>
			<directory>target/</directory>
			<includes>
				<include>LICENSE.txt</include>
			</includes>
			<outputDirectory></outputDirectory>


@@ 43,12 36,5 @@
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
		<fileSet>
			<directory>target/win-libs</directory>
			<includes>
				<include>*.jar</include>
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
	</fileSets>
</assembly>
\ No newline at end of file

M main/buildkit/pom.xml => main/buildkit/pom.xml +144 -102
@@ 1,10 1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.8</version>
		<version>1.5.9</version>
	</parent>
	<artifactId>buildkit</artifactId>
	<packaging>pom</packaging>


@@ 24,7 24,6 @@
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-resources-plugin</artifactId>
				<version>3.1.0</version>
				<executions>
					<execution>
						<id>copy-resources</id>


@@ 55,8 54,8 @@

			<!-- copy libraries to target/libs/: -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-dependency-plugin</artifactId>
				<version>3.1.1</version>
				<executions>
					<execution>
						<id>copy-libs</id>


@@ 65,110 64,153 @@
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<includeScope>runtime</includeScope>
							<outputDirectory>${project.build.directory}/libs</outputDirectory>
							<excludeClassifiers>linux,mac,win</excludeClassifiers>
							<excludeArtifactIds>dbus-java,secret-service,hkdf,java-utils</excludeArtifactIds>
						</configuration>
					</execution>
					<execution>
						<id>copy-linux-libs</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/linux-libs</outputDirectory>
							<includeGroupIds>org.openjfx</includeGroupIds>
							<classifier>linux</classifier>
						</configuration>
					</execution>
					<execution>
						<id>copy-linux-secret-service</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/linux-libs</outputDirectory>
							<includeArtifactIds>dbus-java,secret-service,hkdf,java-utils</includeArtifactIds>
						</configuration>
					</execution>
					<execution>
						<id>copy-mac-libs</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/mac-libs</outputDirectory>
							<includeGroupIds>org.openjfx</includeGroupIds>
							<classifier>mac</classifier>
						</configuration>
					</execution>
					<execution>
						<id>copy-win-libs</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/win-libs</outputDirectory>
							<includeGroupIds>org.openjfx</includeGroupIds>
							<classifier>win</classifier>
						</configuration>
					</execution>
				</executions>
			</plugin>

			<!-- create buildkit.zip: -->
			<plugin>
				<artifactId>maven-assembly-plugin</artifactId>
				<version>3.1.1</version>
				<executions>
					<execution>
						<id>assemble-linux</id>
						<phase>package</phase>
						<goals>
							<goal>single</goal>
						</goals>
						<configuration>
							<descriptors>
								<descriptor>assembly-linux.xml</descriptor>
							</descriptors>
							<appendAssemblyId>false</appendAssemblyId>
							<finalName>buildkit-linux</finalName>
						</configuration>
					</execution>
					<execution>
						<id>assemble-mac</id>
						<phase>package</phase>
						<goals>
							<goal>single</goal>
						</goals>
						<configuration>
							<descriptors>
								<descriptor>assembly-mac.xml</descriptor>
							</descriptors>
							<appendAssemblyId>false</appendAssemblyId>
							<finalName>buildkit-mac</finalName>
						</configuration>
					</execution>
					<execution>
						<id>assemble-win</id>
						<phase>package</phase>
						<goals>
							<goal>single</goal>
						</goals>
						<configuration>
							<descriptors>
								<descriptor>assembly-win.xml</descriptor>
							</descriptors>
							<appendAssemblyId>false</appendAssemblyId>
							<finalName>buildkit-win</finalName>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

	<profiles>
		<profile>
			<id>linux</id>
			<build>
				<plugins>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-assembly-plugin</artifactId>
						<executions>
							<execution>
								<id>assemble-linux</id>
								<phase>package</phase>
								<goals>
									<goal>single</goal>
								</goals>
								<configuration>
									<descriptors>
										<descriptor>assembly-linux.xml</descriptor>
									</descriptors>
									<appendAssemblyId>false</appendAssemblyId>
									<finalName>buildkit-linux</finalName>
								</configuration>
							</execution>
						</executions>
					</plugin>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-dependency-plugin</artifactId>
						<executions>
							<execution>
								<id>copy-linux-libs</id>
								<phase>prepare-package</phase>
								<goals>
									<goal>copy-dependencies</goal>
								</goals>
								<configuration>
									<outputDirectory>${project.build.directory}/libs</outputDirectory>
									<includeGroupIds>org.openjfx</includeGroupIds>
									<classifier>linux</classifier>
								</configuration>
							</execution>
						</executions>
					</plugin>
				</plugins>
			</build>
		</profile>

		<profile>
			<id>mac</id>
			<build>
				<plugins>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-assembly-plugin</artifactId>
						<executions>
							<execution>
								<id>assemble-mac</id>
								<phase>package</phase>
								<goals>
									<goal>single</goal>
								</goals>
								<configuration>
									<descriptors>
										<descriptor>assembly-mac.xml</descriptor>
									</descriptors>
									<appendAssemblyId>false</appendAssemblyId>
									<finalName>buildkit-mac</finalName>
								</configuration>
							</execution>
						</executions>
					</plugin>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-dependency-plugin</artifactId>
						<executions>
							<execution>
								<id>copy-mac-libs</id>
								<phase>prepare-package</phase>
								<goals>
									<goal>copy-dependencies</goal>
								</goals>
								<configuration>
									<outputDirectory>${project.build.directory}/libs</outputDirectory>
									<includeGroupIds>org.openjfx</includeGroupIds>
									<classifier>mac</classifier>
								</configuration>
							</execution>
						</executions>
					</plugin>
				</plugins>
			</build>
		</profile>

		<profile>
			<id>windows</id>
			<build>
				<plugins>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-assembly-plugin</artifactId>
						<executions>
							<execution>
								<id>assemble-win</id>
								<phase>package</phase>
								<goals>
									<goal>single</goal>
								</goals>
								<configuration>
									<descriptors>
										<descriptor>assembly-win.xml</descriptor>
									</descriptors>
									<appendAssemblyId>false</appendAssemblyId>
									<finalName>buildkit-win</finalName>
								</configuration>
							</execution>
						</executions>
					</plugin>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-dependency-plugin</artifactId>
						<executions>
							<execution>
								<id>copy-win-libs</id>
								<phase>prepare-package</phase>
								<goals>
									<goal>copy-dependencies</goal>
								</goals>
								<configuration>
									<outputDirectory>${project.build.directory}/libs</outputDirectory>
									<includeGroupIds>org.openjfx</includeGroupIds>
									<classifier>win</classifier>
								</configuration>
							</execution>
						</executions>
					</plugin>
				</plugins>
			</build>
		</profile>
	</profiles>
</project>
\ No newline at end of file

D main/buildkit/src/main/resources/ffi-version.txt => main/buildkit/src/main/resources/ffi-version.txt +0 -1
@@ 1,1 0,0 @@
${cryptomator.jni.version}
\ No newline at end of file

M main/buildkit/src/main/resources/launcher-linux.sh => main/buildkit/src/main/resources/launcher-linux.sh +3 -2
@@ 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

M main/buildkit/src/main/resources/launcher-mac.sh => main/buildkit/src/main/resources/launcher-mac.sh +1 -0
@@ 1,4 1,5 @@
#!/bin/sh
cd $(dirname $0)
java \
	-cp "libs/*" \
	-Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" \

M main/buildkit/src/main/resources/launcher-win.bat => main/buildkit/src/main/resources/launcher-win.bat +1 -0
@@ 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 ^

M main/commons/pom.xml => main/commons/pom.xml +4 -4
@@ 1,10 1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.8</version>
		<version>1.5.9</version>
	</parent>
	<artifactId>commons</artifactId>
	<name>Cryptomator Commons</name>


@@ 29,7 29,7 @@
		</dependency>
		<dependency>
			<groupId>org.cryptomator</groupId>
			<artifactId>jni</artifactId>
			<artifactId>integrations-api</artifactId>
		</dependency>

		<!-- JavaFx -->


@@ 44,7 44,7 @@

		<!-- EasyBind -->
		<dependency>
			<groupId>org.fxmisc.easybind</groupId>
			<groupId>com.tobiasdiez</groupId>
			<artifactId>easybind</artifactId>
		</dependency>


M main/commons/src/main/java/org/cryptomator/common/CommonsModule.java => main/commons/src/main/java/org/cryptomator/common/CommonsModule.java +6 -5
@@ 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);

M main/commons/src/main/java/org/cryptomator/common/Environment.java => main/commons/src/main/java/org/cryptomator/common/Environment.java +5 -0
@@ 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 {

D main/commons/src/main/java/org/cryptomator/common/JniModule.java => main/commons/src/main/java/org/cryptomator/common/JniModule.java +0 -32
@@ 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<MacFunctions> provideOptionalMacFunctions() {
		return JniFunctions.macFunctions();
	}

	@Provides
	@Singleton
	Optional<WinFunctions> provideOptionalWinFunctions() {
		return JniFunctions.winFunctions();
	}

}

M main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java => main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java +4 -4
@@ 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 <T> 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 <T> Type of the value
	 * @param <E> Type of the any expected exception that may occur during initialization
	 * @param reference A reference to a maybe not yet initialized value.

M main/commons/src/main/java/org/cryptomator/common/LicenseHolder.java => main/commons/src/main/java/org/cryptomator/common/LicenseHolder.java +4 -4
@@ 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

M main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java => main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java +2 -2
@@ 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 <a href="http://semver.org/spec/v2.0.0.html">SemVer 2.0.0</a>.
 */

R main/keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java => main/commons/src/main/java/org/cryptomator/common/keychain/KeychainManager.java +31 -22
@@ 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<KeychainAccessProvider> keychain;
	private final LoadingCache<String, BooleanProperty> passphraseStoredProperties;

	private final KeychainAccessStrategy keychain;
	private LoadingCache<String, BooleanProperty> passphraseStoredProperties;

	KeychainManager(KeychainAccessStrategy keychain) {
		assert keychain.isSupported();
		this.keychain = keychain;
	@Inject
	KeychainManager(ObjectExpression<KeychainAccessProvider> 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 <code>true</code> when it almost certain that a password for <code>key</code> 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);

A main/commons/src/main/java/org/cryptomator/common/keychain/KeychainModule.java => main/commons/src/main/java/org/cryptomator/common/keychain/KeychainModule.java +44 -0
@@ 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<ServiceLoader.Provider<KeychainAccessProvider>> provideAvailableKeychainAccessProviderFactories() {
		return ServiceLoader.load(KeychainAccessProvider.class).stream().collect(Collectors.toUnmodifiableSet());
	}

	@Provides
	@Singleton
	static Set<KeychainAccessProvider> provideSupportedKeychainAccessProviders(Set<ServiceLoader.Provider<KeychainAccessProvider>> availableFactories) {
		return availableFactories.stream() //
				.map(ServiceLoader.Provider::get) //
				.filter(KeychainAccessProvider::isSupported) //
				.collect(Collectors.toUnmodifiableSet());
	}

	@Provides
	@Singleton
	static ObjectExpression<KeychainAccessProvider> provideKeychainAccessProvider(Settings settings, Set<KeychainAccessProvider> 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());
	}

}

A main/commons/src/main/java/org/cryptomator/common/keychain/NoKeychainAccessProviderException.java => main/commons/src/main/java/org/cryptomator/common/keychain/NoKeychainAccessProviderException.java +13 -0
@@ 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.");
	}
}

A main/commons/src/main/java/org/cryptomator/common/mountpoint/AvailableDriveLetterChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/AvailableDriveLetterChooser.java +36 -0
@@ 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<Path> chooseMountPoint(Volume caller) {
		return this.windowsDriveLetters.getAvailableDriveLetterPath();
	}

	@Override
	public int getPriority() {
		return PRIORITY;
	}
}

A main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomDriveLetterChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomDriveLetterChooser.java +37 -0
@@ 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<Path> chooseMountPoint(Volume caller) {
		return this.vaultSettings.getWinDriveLetter().map(letter -> letter.charAt(0) + ":\\").map(Paths::get);
	}

	@Override
	public int getPriority() {
		return PRIORITY;
	}
}

A main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +101 -0
@@ 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<Path> 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<Path> 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;
	}
}

A main/commons/src/main/java/org/cryptomator/common/mountpoint/InvalidMountPointException.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/InvalidMountPointException.java +16 -0
@@ 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);
	}
}

A main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java +64 -0
@@ 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);
		}
	}

}

A main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java +169 -0
@@ 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.<br>
 * <p>All <i>MountPointChoosers (MPCs)</i> 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.<br>
 * A specific priority <b>must not</b> be assigned to more than one MPC at a time;
 * the result of having two MPCs with equal priority is undefined.
 *
 * <p>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
 * <i>before</i> calling {@code #chooseMountPoint(Volume)}.
 *
 * <p>At execution of a MPC {@link #chooseMountPoint(Volume)} is called to choose a mountpoint
 * according to the MPC's <i>strategy.</i> The <i>strategy</i> 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 <i>no other MPC's will be called for
 * this volume, even if {@code #prepare(Volume, Path)} fails.</i>
 *
 * <p>If {@code #chooseMountPoint(Volume)} yields no result, the next MPC is executed
 * <i>without</i> first calling the {@code #prepare(Volume, Path)} method of the current MPC.
 * This is repeated until<br>
 * <ul>
 *     <li><b>either</b> a mountpoint is returned by {@code #chooseMountPoint(Volume)}
 *     and {@code #prepare(Volume, Path)} succeeds or fails, ending the entire operation</li>
 *     <li><b>or</b> no MPC remains and an {@link InvalidMountPointException} is thrown.</li>
 * </ul>
 * 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<MountPointChooser> {

	/**
	 * Called by the {@link Volume} to determine whether this MountPointChooser is
	 * applicable for mounting the Vault/Volume, especially with regard to the
	 * current system configuration and particularities of the Volume type.
	 *
	 * <p>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.
	 *
	 * <p>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 <b>without</b> 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<Path> chooseMountPoint(Volume caller);

	/**
	 * Called by a {@link Volume} to prepare and/or verify the chosen mountpoint.<br>
	 * This method is only called if the {@link #chooseMountPoint(Volume)} method
	 * of the same MountPointChooser returned a path.
	 *
	 * <p>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.
	 *
	 * <p>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.
	 *
	 * <p><b>Please note:</b> 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.
	 *
	 * <p>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.<br>
	 * 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.<br>
	 * A specific priority <b>must not</b> 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.
	 * <b>Implementations usually may not override this method.</b> This default implementation
	 * sorts the MPCs in ascending order of their {@link #getPriority() priority.}<br>
	 * <br>
	 * <b>Original description:</b>
	 * <p>{@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());
	}
}

A main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java +50 -0
@@ 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.}<br>
 * 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<MountPointChooser> provideOrderedMountPointChoosers(Set<MountPointChooser> choosers) {
		//Sort by natural order. The natural order is defined by MountPointChooser#compareTo
		return ImmutableSortedSet.copyOf(choosers);
	}
}

A main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java +121 -0
@@ 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<Path> 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;
	}
}

A main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java => main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java +19 -0
@@ 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;
	}

}

M main/commons/src/main/java/org/cryptomator/common/settings/Settings.java => main/commons/src/main/java/org/cryptomator/common/settings/Settings.java +10 -4
@@ 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<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
	private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
	private final ObjectProperty<KeychainBackend> keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND);
	private final ObjectProperty<NodeOrientation> 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<Settings> 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> keychainBackend() { return keychainBackend; }

	public ObjectProperty<NodeOrientation> userInterfaceOrientation() {
		return userInterfaceOrientation;
	}

M main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java => main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java +12 -1
@@ 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<Settings> {
		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<Settings> {
				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<Settings> {
		}
	}

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

M main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java => main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java +21 -17
@@ 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<String> getWinDriveLetter() {
		String current = this.winDriveLetter.get();
		if (!Strings.isNullOrEmpty(current)) {
			return Optional.of(current);
		}
		return Optional.empty();
	}

	public BooleanProperty unlockAfterStartup() {
		return unlockAfterStartup;
	}

M main/commons/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java => main/commons/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java +2 -1
@@ 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;
	}


A main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java => main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java +60 -0
@@ 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<MountPointChooser> choosers;

	protected Path mountPoint;

	//Cleanup
	private boolean cleanupRequired;
	private MountPointChooser usedChooser;

	public AbstractVolume(SortedSet<MountPointChooser> choosers) {
		this.choosers = choosers;
	}

	protected Path determineMountPoint() throws InvalidMountPointException {
		SortedSet<MountPointChooser> checkedChoosers = new TreeSet<>(); //Natural order
		for (MountPointChooser chooser : this.choosers) {
			if (!chooser.isApplicable(this)) {
				continue;
			}

			Optional<Path> 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() ? "<No applicable MPC>" : checked));
	}

	protected void cleanupMountPoint() {
		if (this.cleanupRequired) {
			this.usedChooser.cleanup(this, this.mountPoint);
		}
	}

	@Override
	public Optional<Path> getMountPoint() {
		return Optional.ofNullable(mountPoint);
	}
}

M main/commons/src/main/java/org/cryptomator/common/vaults/DefaultMountFlags.java => main/commons/src/main/java/org/cryptomator/common/vaults/DefaultMountFlags.java +1 -0
@@ 10,4 10,5 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Retention(RUNTIME)
public @interface DefaultMountFlags {

}

M main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java => main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java +22 -51
@@ 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<MountPointChooser> 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<String> 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<Path> 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 boolean isSupported() {
		return DokanyVolume.isSupportedStatic();
	}

	@Override
	public Optional<Path> getMountPoint() {
		return Optional.ofNullable(mountPoint);
	public MountPointRequirement getMountPointRequirement() {
		return MountPointRequirement.EMPTY_MOUNT_POINT;
	}

	public static boolean isSupportedStatic() {

M main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java => main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java +29 -93
@@ 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,85 16,26 @@ 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<MountPointChooser> choosers) {
		super(choosers);
	}

	@Override
	public void mount(CryptoFileSystem fs, String mountFlags) throws IOException, FuseNotSupportedException, VolumeException {
		Optional<String> 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);
		}
		mount(fs.getPath("/"), mountFlags);
	}

	private void checkProvidedMountPoint(Path mountPoint) throws IOException {
		if (!Files.isDirectory(mountPoint)) {
			throw new NotDirectoryException(mountPoint.toString());
		}
		try (DirectoryStream<Path> 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;
		}
	}
	public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
		this.mountPoint = determineMountPoint();

	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.");
		mount(fs.getPath("/"), mountFlags);
	}

	private void mount(Path root, String mountFlags) throws VolumeException {


@@ 102,8 44,8 @@ public class FuseVolume implements Volume {
			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<Path> 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();
	}

}

A main/commons/src/main/java/org/cryptomator/common/vaults/MountPointRequirement.java => main/commons/src/main/java/org/cryptomator/common/vaults/MountPointRequirement.java +28 -0
@@ 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

M main/commons/src/main/java/org/cryptomator/common/vaults/PerVault.java => main/commons/src/main/java/org/cryptomator/common/vaults/PerVault.java +1 -1
@@ 8,6 8,6 @@ import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface PerVault {
public @interface PerVault {

}

M main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java => main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java +28 -12
@@ 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<Volume> getVolume() {
		return Optional.ofNullable(this.volume);
	}

	// ******************************************************************************
	// Hashcode / Equals
	// *******************************************************************************/

M main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java => main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java +3 -3
@@ 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();

M main/commons/src/main/java/org/cryptomator/common/vaults/VaultListChangeListener.java => main/commons/src/main/java/org/cryptomator/common/vaults/VaultListChangeListener.java +3 -3
@@ 1,9 1,9 @@
package org.cryptomator.common.vaults;

import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.cryptomator.common.settings.VaultSettings;

import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.List;
import java.util.stream.Collectors;



@@ 20,7 20,7 @@ class VaultListChangeListener implements ListChangeListener<Vault> {

	@Override
	public void onChanged(Change<? extends Vault> c) {
		while(c.next()) {
		while (c.next()) {
			if (c.wasAdded()) {
				List<VaultSettings> addedSettings = c.getAddedSubList().stream().map(Vault::getVaultSettings).collect(Collectors.toList());
				vaultSettingsList.addAll(c.getFrom(), addedSettings);

M main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java => main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +2 -2
@@ 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;

M main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java => main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java +30 -6
@@ 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;

M main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java => main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java +1 -1
@@ 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,


M main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java => main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java +89 -8
@@ 1,19 1,21 @@
package org.cryptomator.common.vaults;

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 org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemStats;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
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<Optional<CryptoFileSystemStats>> 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<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) {


@@ 53,12 64,36 @@ public class VaultStats {

	private void updateStats(Optional<CryptoFileSystemStats> 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<Optional<CryptoFileSystemStats>> {

		private UpdateStatsService() {
			setOnFailed(event -> LOG.error("Error in UpdateStateService.", getException()));
		}

		@Override
		protected Task<Optional<CryptoFileSystemStats>> 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();}
}

M main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java => main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java +11 -1
@@ 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;



@@ 21,10 22,17 @@ 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<Path> getMountPoint();

	MountPointRequirement getMountPointRequirement();

	// optional forced unmounting:

	default boolean supportsForcedUnmount() {

M main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java => main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java +15 -1
@@ 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);


@@ 127,6 136,11 @@ public class WebDavVolume implements Volume {
	}

	@Override
	public VolumeImpl getImplementationType() {
		return VolumeImpl.WEBDAV;
	}

	@Override
	public boolean supportsForcedUnmount() {
		return mount != null && mount.forced().isPresent();
	}

M main/commons/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java => main/commons/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java +15 -2
@@ 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<String> getAvailableDriveLetters() {
		return Sets.difference(C_TO_Z, getOccupiedDriveLetters());
		return Sets.difference(getAllDriveLetters(), getOccupiedDriveLetters());
	}

	public Optional<String> getAvailableDriveLetter() {
		return getAvailableDriveLetters().stream().findFirst();
	}

	public Optional<Path> getAvailableDriveLetterPath() {
		return getAvailableDriveLetter().map(this::toPath);
	}

	public Path toPath(String driveLetter) {
		return Path.of(driveLetter + ":\\");
	}
}

M main/commons/src/test/java/org/cryptomator/common/EnvironmentTest.java => main/commons/src/test/java/org/cryptomator/common/EnvironmentTest.java +4 -4
@@ 37,7 37,7 @@ class EnvironmentTest {

		List<Path> 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<Path> 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<Path> 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")));
		}


M main/commons/src/test/java/org/cryptomator/common/LicenseCheckerTest.java => main/commons/src/test/java/org/cryptomator/common/LicenseCheckerTest.java +3 -4
@@ 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<DecodedJWT> decoded = licenseChecker.check(license);
		

		Assertions.assertTrue(decoded.isPresent());
		Assertions.assertEquals("cryptobot@example.com", decoded.get().getSubject());
	}

R main/keychain/src/test/java/org/cryptomator/keychain/KeychainManagerTest.java => main/commons/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java +15 -12
@@ 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

R main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java => main/commons/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java +4 -2
@@ 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<String, char[]> map = new HashMap<>();


M main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java => main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java +4 -4
@@ 37,10 37,10 @@ public class SettingsJsonAdapterTest {
	}

	@ParameterizedTest(name = "fromJson() should throw IOException for input: {0}")
	@ValueSource(strings = {
			"",
			"<html>",
			"{invalidjson}"
	@ValueSource(strings = { //
			"", //
			"<html>", //
			"{invalidjson}" //
	})
	public void testDeserializeMalformed(String input) {
		Assertions.assertThrows(IOException.class, () -> {

M main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java => main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java +1 -1
@@ 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\""));

M main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsTest.java => main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsTest.java +1 -1
@@ 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);

M main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java => main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java +6 -4
@@ 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());
	}


D main/keychain/pom.xml => main/keychain/pom.xml +0 -63
@@ 1,63 0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.8</version>
	</parent>
	<artifactId>keychain</artifactId>
	<name>System Keychain Access</name>

	<dependencies>
		<dependency>
			<groupId>org.cryptomator</groupId>
			<artifactId>commons</artifactId>
		</dependency>

		<!-- JavaFx -->
		<dependency>
			<groupId>org.openjfx</groupId>
			<artifactId>javafx-base</artifactId>
		</dependency>
		<dependency>
			<groupId>org.openjfx</groupId>
			<artifactId>javafx-graphics</artifactId>
		</dependency>
		
		<!-- Apache -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
		</dependency>

		<!-- Google -->
		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
		</dependency>
		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
		</dependency>

		<!-- DI -->
		<dependency>
			<groupId>com.google.dagger</groupId>
			<artifactId>dagger</artifactId>
		</dependency>

		<!-- secret-service lib -->
		<dependency>
			<groupId>de.swiesend</groupId>
			<artifactId>secret-service</artifactId>
		</dependency>

		<!-- Logging -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-simple</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
</project>
\ No newline at end of file

D main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessException.java => main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessException.java +0 -12
@@ 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);
	}

}

D main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java => main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java +0 -46
@@ 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 <code>null</code> 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 <code>true</code> if this KeychainAccessStrategy works on the current machine.
	 * @implNote This method must not throw any exceptions and should fail fast
	 * returning <code>false</code> if it can't determine availability of the checked strategy
	 */
	boolean isSupported();

}

D main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java => main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java +0 -45
@@ 1,45 0,0 @@
/*******************************************************************************