Compare commits

...
120 Commits
Author SHA1 Message Date
Armin Schrenk 8f42c1c9ce Merge branch 'develop' into release/1.7.0 2022-11-18 10:47:33 +01:00
Armin SchrenkandGitHub 37f89fd235 Move release precondition checks to own workflow (#2535) 2022-11-18 10:46:22 +01:00
Armin Schrenk 668feb73f9 Merge branch 'feature/mount-provider' into release/1.7.0 2022-11-17 09:59:46 +01:00
Armin Schrenk ba0569deb1 preparing 1.7.0 2022-11-17 09:59:38 +01:00
Armin Schrenk a70d8e1567 update nonModularGroupIds 2022-11-17 09:50:39 +01:00
Armin Schrenk 96b7a8df3f bump dokany-nio-adapter 2022-11-17 09:24:51 +01:00
Armin Schrenk 5143fdccbb Refactor mount point setting and controller :
* remove CustomMountPath, winDriveLetter and usesCustomMountPath
* new property mountPoint (can be null)
* differentiation between driveLetter and directory happens in controller
2022-11-11 19:42:59 +01:00
Armin Schrenk 3c5502955a bump version of dokany-nio-adapter 2022-11-11 15:20:13 +01:00
Armin Schrenk 2d3a035d9e renamed mount option controller members 2022-11-11 11:44:50 +01:00
Sebastian Stenzel 4029f86a0d reveal OR copy mount point depending on chosen mounter 2022-11-10 14:11:06 +01:00
Sebastian Stenzel 6dac00625c updated mount API 2022-11-09 15:33:20 +01:00
Sebastian Stenzel 41b7bd54f1 use vault id as a volume id 2022-11-08 16:38:45 +01:00
Sebastian Stenzel f236614bd0 allow repeated mount & unmount of the same vault 2022-11-08 16:38:31 +01:00
Sebastian Stenzel 31c69f145e use a fallback mount point absed on -Dcryptomator.mountPointsDir=... vm param 2022-11-07 18:17:20 +01:00
Sebastian Stenzel b5a00f5ebe added --enable-preview --enable-native-access to run config 2022-11-07 18:07:45 +01:00
Sebastian Stenzel 40c4300012 fixed json syntax error 2022-11-07 16:20:19 +01:00
Sebastian Stenzel 5369ddb1f9 adjust suppression.xml to jfuse 2022-11-07 16:17:50 +01:00
Sebastian Stenzel 1b4117de38 volume settings → change tcp port 2022-11-07 16:16:10 +01:00
Sebastian Stenzel 3e5a7fadd0 only set the mount point when a mount point is explicitly required
(didn't work for the webdav fallback provider, which doesn't _not_ MOUNT_TO_SYSTEM_CHOSEN_PATH, as it does not mount at all)
2022-11-07 15:55:26 +01:00
Sebastian Stenzel eff2530e70 pre-select chosen mount service 2022-11-07 15:52:48 +01:00
Armin Schrenk 0f84d0c990 adapt to new APIs 2022-11-07 14:49:56 +01:00
Armin Schrenk fbe33e21fb Fixes #2498
Bump winfsp for windows bundle distribution
2022-11-03 10:16:53 +01:00
Sebastian StenzelandGitHub 7091839f08 Merge pull request #2505 from cryptomator/feature/javafx-19 2022-10-28 22:09:29 +02:00
Sebastian StenzelandGitHub 79bf1759c7 Merge pull request #2509 from cryptomator/feature/jdk19 2022-10-28 22:09:06 +02:00
Sebastian Stenzel 70e5f53009 Merge branch 'develop' into feature/jdk19 2022-10-27 18:13:42 +02:00
Sebastian Stenzel c79cfe1c9d Merge branch 'develop' into feature/javafx-19
# Conflicts:
#	pom.xml
2022-10-26 11:43:07 +02:00
Sebastian Stenzel d619a0cbf4 bump jwt version 2022-10-26 11:40:09 +02:00
Sebastian StenzelandGitHub 517b5958b0 Merge pull request #2487 from cryptomator/feature/ppa-jdk-19
Use `coffeelibs-jdk-19` during PPA build
2022-10-18 16:48:28 +02:00
Armin Schrenk 01a6475d5f replace deprecated method call 2022-10-17 17:13:12 +02:00
Sebastian Stenzel 69641ed3b6 Merge branch 'develop' into feature/jdk19
[ci skip]
2022-10-14 13:55:15 +02:00
Sebastian Stenzel 1f01923169 build on Ubuntu Focal 2022-10-14 13:44:50 +02:00
Sebastian Stenzel 687bed6984 use the correct name of required build-dependency 2022-10-14 13:24:08 +02:00
Sebastian Stenzel 01698d1393 install required build-dependency 2022-10-14 13:21:28 +02:00
Sebastian Stenzel 6367cc2dfb depend on coffeelibs-jdk-19 2022-10-14 13:10:37 +02:00
Armin Schrenk bcfda68bef bump used gh actions 2022-10-13 12:48:48 +02:00
Sebastian Stenzel 2399e29d75 update logback 2022-10-12 13:42:11 +02:00
322779ee88 Refactor winget release to own workflow file (#2471)
... and call it on release windows build

Co-authored-by: Sebastian Stenzel <overheadhunter@users.noreply.github.com>
2022-10-11 20:16:24 +02:00
Julian Raufelder fd6eb2139d Minor cleanup in build.yml 2022-10-11 17:45:59 +02:00
Julian Raufelder 26ecd67a79 Update linux common metainfo.xml file 2022-10-11 15:33:44 +02:00
Julian Raufelder e06240cbb9 Merge pull request #2475 from cryptomator/feature/add-release-check-precondition
Check POM version and linux metainfo.xml file during release steps
2022-10-11 15:11:54 +02:00
Julian Raufelder b10d892b12 Skip release tests too when ci-skip provided in commit message 2022-10-11 15:09:19 +02:00
Julian Raufelder 45c2dd0358 Validate release check precondition in separate job 2022-10-11 12:45:49 +02:00
Julian Raufelder 34d6e8c419 Validate POM version and linux metainfo.xml file during build 2022-10-10 15:28:16 +02:00
Julian Raufelder ae6a15b391 Check POM version and linux metainfo.xml file during release steps 2022-10-10 14:49:36 +02:00
Sebastian Stenzel 8040b71a51 simplified some more bindings 2022-10-10 12:09:30 +02:00
Sebastian Stenzel 76e7a0a7b8 bump OpenJFX to version 19
replaced EasyBind with `ObservableValue.map()` where possible
2022-10-10 10:24:42 +02:00
Sebastian Stenzel 910efc0b7b prepare macOS builds for diverging Xcode versions
[ci skip]
2022-10-07 12:03:00 +02:00
Sebastian Stenzel c805a820a4 Merge branch 'main' into develop
[ci skip]
2022-10-06 17:42:00 +02:00
Sebastian Stenzel 3ed8316b60 Merge branch 'release/1.6.15' 2022-10-06 17:40:22 +02:00
Sebastian Stenzel b6e381f0bf apply cryptofs hotfix 2022-10-06 17:37:03 +02:00
Sebastian Stenzel aef3f4c6e2 prepare 1.6.15 2022-10-06 16:49:12 +02:00
Sebastian Stenzel 7bc5a336e4 dependency bump 2022-10-06 16:48:12 +02:00
Sebastian Stenzel 672b4aee2d added Belgian, Danish, Farsi, Sinhala, Vietnamese to language chooser 2022-10-06 16:19:16 +02:00
CryptobotandGitHub d4ab17b38c New Crowdin updates (#2427)
[ci skip]
2022-10-06 16:11:29 +02:00
Sebastian Stenzel 91f49c5642 update cryptolib + cryptofs
fixes #2451
2022-10-06 15:20:21 +02:00
Julian Raufelder da2f97b1c4 Fix communitcation with hub if it supports only HTTP version 1.1 2022-10-06 09:38:02 +02:00
Tobias HagemannandGitHub 5d647d20a5 Update README.md 2022-09-22 15:00:22 +02:00
Armin SchrenkandGitHub ce96d1c65e Merge pull request #2452 from chewitt1/chewitt-next-button
Fixes #2447
2022-09-21 21:31:33 +02:00
Carrima Hewitt f02a8e14ff Fixed Trim and Bidirectional Bind 2022-09-21 11:11:00 -04:00
Armin Schrenk dd31908f3a Use for windows exe and msi build same JDKs 2022-09-21 15:50:08 +02:00
Sebastian Stenzel 495a1884c3 Merge branch 'develop' into feature/jdk19
[ci skip]
2022-09-21 13:21:18 +02:00
Sebastian Stenzel 49c3b1fa43 fix font loading warning visible in console
see https://stackoverflow.com/a/65127949/4014509
2022-09-21 13:20:34 +02:00
Sebastian Stenzel cde67966da fix test 2022-09-21 09:49:57 +02:00
Sebastian Stenzel d19e77432e build with jdk 19 2022-09-21 09:28:37 +02:00
Sebastian StenzelandGitHub cc24a4c9ae Merge pull request #2445 from purejava/disable-keyrings
fixes #2047, fixes #2219
2022-09-21 09:12:40 +02:00
Ralph Plawetzki 1101a7574c Beautify the code 2022-09-20 20:12:44 +02:00
Ralph Plawetzki 30de04e291 Move CheckBox (injected from JavaFX) below the final fields (injected by Dagger) 2022-09-20 20:12:43 +02:00
Ralph Plawetzki 2e3d2e86e2 Cut off the application from accessing keychains at the earliest point possible
as suggested in the discussion to #2445
2022-09-20 20:12:43 +02:00
Ralph Plawetzki 78cf082096 Rename properties as suggested in the discussion to #2445 2022-09-20 20:12:43 +02:00
Ralph Plawetzki 64c90f8107 Move checkbox to the front and rename members 2022-09-20 20:12:43 +02:00
Ralph Plawetzki b34bf6f161 Handle further queries of the backend for disabled keyrings 2022-09-20 20:12:43 +02:00
Ralph Plawetzki 17d3d7307d Wire disable all keyrings setting checkbox to the KeychainManager 2022-09-20 20:12:43 +02:00
Ralph Plawetzki e6c7fed662 Add disable all keyrings setting checkbox
and connect it to the settings file
2022-09-20 20:12:42 +02:00
Ralph Plawetzki 23fad16742 Revert "Add disable all keyrings setting checkbox"
This reverts commit d037c6bc140495b5469c2a25ae2a7a2752bc35dc.
2022-09-20 20:12:42 +02:00
Ralph Plawetzki ec794cdca2 Add disable all keyrings setting checkbox
and connect it to the settings file
2022-09-20 20:12:42 +02:00
Carrima Hewitt 2def9216ec Trimmed Vault Name StringProperty 2022-09-20 10:33:26 -04:00
Carrima Hewitt 4f9759af58 Merge branch 'chewitt-next-button' of https://github.com/chewitt1/cryptomator into chewitt-next-button 2022-09-20 10:20:10 -04:00
Carrima Hewitt 382f6b7851 Reverted Misc 2022-09-20 10:19:38 -04:00
Carrima HewittandGitHub d60eb470ea Merge branch 'cryptomator:develop' into chewitt-next-button 2022-09-20 08:41:38 -04:00
Carrima Hewitt 97a61ef717 Removed Comments 2022-09-20 08:37:38 -04:00
Armin Schrenk 08fc5e9a77 Fixes #2454 2022-09-20 10:58:48 +02:00
Carrima Hewitt 4c96c285bf Fixed Next button for Invalid Vault Name 2022-09-19 21:17:48 -04:00
Edward ChowandGitHub 0021f2d37c Use 2x icon on Mac menu bar (#2345) (#2346)
* Use 2x icon on Mac menu bar

Use 2x icon on Mac menu bar

* Fix #2357

Changed Windows tray icon to 2x
2022-09-15 17:38:42 +02:00
Tobias HagemannandGitHub 31d32f2018 Added macOS ARM64 workflow (#2432) 2022-09-10 08:30:18 +02:00
Armin SchrenkandGitHub b9c087b48e Merge pull request #2430 from youscam/patch-1
Update README.md
2022-09-07 09:53:52 +02:00
KAMAL AZZOUZIandGitHub 797dbc0a57 Update README.md
Update Windows profile name
2022-09-07 00:52:40 +01:00
Armin Schrenk 46745d030b [ci skip] fix winget release action 2022-08-31 18:12:59 +02:00
Armin Schrenk 3cdf124e54 Merge branch 'main' into develop
[ci skip]
2022-08-31 16:02:28 +02:00
Armin Schrenk d37147c73e Merge branch 'release/1.6.14' 2022-08-31 15:59:56 +02:00
Armin Schrenk 67dd1548cc finalize release 2022-08-31 15:47:23 +02:00
Armin Schrenk 008bb38e30 preparing 1.6.14 2022-08-31 15:41:18 +02:00
CryptobotandGitHub 1be6128523 New Crowdin updates (#2350)
New translations strings.properties

Arabic; Belarusian; Bengali; Bosnian; Catalan; Chinese Simplified; Chinese Traditional; Chinese Traditional (Hong Kong); Croatian (Cyrillic); Czech; Danish; Dutch; Filipino; French; Galician; German; Greek; Hebrew; Hindi; Hungarian; Indonesian; Italian; Japanese; Korean (Latin); Latvian; Macedonian; Norwegian Bokmal; Norwegian Nynorsk; Persian; Polish; Portuguese; Portuguese, Brazilian; Punjabi; Romanian; Russian; Sinhala; Slovak; Spanish; Swahili, Tanzania; Swedish; Tamil; Telugu; Thai; Turkish; Ukrainian; Vietnamese; 

[ci skip]
2022-08-31 15:17:19 +02:00
Armin Schrenk b4f95c465a Fixes #2408 2022-08-31 13:35:27 +02:00
Sebastian Stenzel 40fa961b9b clean up module-info 2022-08-31 11:40:18 +02:00
Sebastian Stenzel 659123fe7a regenerate keypair if .p12 _or_ keychain item is missing
it used to depend only on the keychain item
2022-08-31 11:25:25 +02:00
Sebastian Stenzel 2448963161 class does not need to be generic 2022-08-31 10:57:41 +02:00
Sebastian Stenzel 16c0431f6f use release versions of slf4j + logback 2022-08-31 10:52:53 +02:00
Sebastian Stenzel 2d99d1bdd4 Adding EaseUS as a sponsor
[ci skip]
2022-08-31 10:46:11 +02:00
Sebastian StenzelandGitHub 86ed0d3c0e Merge pull request #2390 from cryptomator/feature/slf4j-2
Upgrade to SLF4J 2.x and Logback 1.3.x
2022-08-12 09:53:27 +02:00
Sebastian Stenzel 0501b0ef39 fixed test 2022-08-11 18:23:33 +02:00
Sebastian Stenzel 2c2b208de7 no need to reset anything, since there is no previous config 2022-08-11 18:18:48 +02:00
Sebastian Stenzel ec7ad0f479 use new ServiceLoader based log configurator 2022-08-11 18:14:54 +02:00
Sebastian Stenzel 5267a04491 make org.cryptomator.desktop an open module 2022-08-11 17:36:10 +02:00
Sebastian Stenzel 04714cd0de make singleton Environment accessible outside of Dagger graph 2022-08-11 17:35:35 +02:00
Sebastian Stenzel 46dbe15e4a update dependencies 2022-08-11 17:34:34 +02:00
Armin SchrenkandGitHub 7abf167ba2 Merge pull request #2381 from cryptomator/feature/hub-handle-402
handle 402 error when authenticating with Cryptomator Hub
2022-08-09 10:55:41 +02:00
44d19bdefe Apply suggestions from code review
Co-authored-by: Tobias Hagemann <tobias.hagemann@skymatic.de>
2022-08-09 10:37:22 +02:00
Armin Schrenk f84eef5639 actually use the scene in ui flow 2022-08-08 16:37:30 +02:00
Armin Schrenk 281f6f727f show info for 402 response on unlock 2022-08-08 16:33:07 +02:00
Sebastian Stenzel bf83df3cd3 Merge branch 'main' into develop
# Conflicts:
#	pom.xml

[ci skip]
2022-08-05 14:31:59 +02:00
Sebastian Stenzel 649e029d4f added cryptomator.integrationsMac.keychainServiceName system property 2022-08-05 11:09:49 +02:00
Sebastian StenzelandGitHub 1cd3d1d67f Merge pull request #2365 from purejava/fix-2301
Pass on CLI parameters from launch scripts to executables
2022-08-02 08:44:04 +02:00
Ralph Plawetzki 8290df9ea6 Pass on CLI parameters for Cryptomator started as an AppImage or installed from PPA
Fixes #2301
2022-08-01 18:17:41 +02:00
Armin Schrenk 47f2d423b9 Remove code smells 2022-07-28 11:16:59 +02:00
Armin Schrenk 80b5b6af00 simplify app version code 2022-07-28 10:58:50 +02:00
Armin Schrenk 414c4e20b8 remove code smell 2022-07-28 10:47:46 +02:00
Armin Schrenk 11a55f382a fix winget release 2022-07-27 17:16:33 +02:00
Armin Schrenk 0c5021913c Merge branch 'main' into develop 2022-07-27 12:11:13 +02:00
Armin Schrenk 2ca824696c Merge branch 'main' into develop 2022-07-26 13:24:11 +02:00
Armin Schrenk 6e75f1fea8 Merge branch 'main' into develop 2022-07-26 13:11:13 +02:00
183 changed files with 2878 additions and 2920 deletions
+4 -4
View File
@@ -10,20 +10,20 @@ on:
required: false required: false
env: env:
JAVA_VERSION: 17 JAVA_VERSION: 19
jobs: jobs:
build: build:
name: Build AppImage name: Build AppImage
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v2 uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: 'zulu'
java-version: ${{ env.JAVA_VERSION }} java-version: ${{ env.JAVA_VERSION }}
cache: 'maven' cache: 'maven'
- id: versions - id: versions
+6 -7
View File
@@ -6,26 +6,25 @@ on:
types: [labeled] types: [labeled]
env: env:
JAVA_VERSION: 17 JAVA_VERSION: 19
defaults: defaults:
run: run:
shell: bash shell: bash
jobs: jobs:
test: test:
name: Compile and Test name: Compile and Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-java@v2 - uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: 'zulu'
java-version: ${{ env.JAVA_VERSION }} java-version: ${{ env.JAVA_VERSION }}
cache: 'maven' cache: 'maven'
- name: Cache SonarCloud packages - name: Cache SonarCloud packages
uses: actions/cache@v2 uses: actions/cache@v3
with: with:
path: ~/.sonar/cache path: ~/.sonar/cache
key: ${{ runner.os }}-sonar key: ${{ runner.os }}-sonar
+7 -6
View File
@@ -15,24 +15,25 @@ on:
required: false required: false
env: env:
JAVA_VERSION: 17 JAVA_VERSION: 19
jobs: jobs:
build: build:
name: Build Debian Package name: Build Debian Package
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install build tools - name: Install build tools
run: | run: |
sudo add-apt-repository ppa:coffeelibs/openjdk
sudo apt-get update sudo apt-get update
sudo apt-get install debhelper devscripts dput sudo apt-get install debhelper devscripts dput coffeelibs-jdk-19
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v2 uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: 'zulu'
java-version: ${{ env.JAVA_VERSION }} java-version: ${{ env.JAVA_VERSION }}
cache: 'maven' cache: 'maven'
- id: versions - id: versions
+24 -12
View File
@@ -10,23 +10,36 @@ on:
required: false required: false
env: env:
JAVA_VERSION: 17 JAVA_VERSION: 19
jobs: jobs:
build: build:
name: Build Cryptomator.app name: Build Cryptomator.app for ${{ matrix.output-suffix }}
runs-on: macos-11 runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-11
architecture: x64
output-suffix: x64
xcode-path: '/Applications/Xcode_13.2.1.app'
- os: [self-hosted, macOS, ARM64]
architecture: aarch64
output-suffix: arm64
xcode-path: '/Applications/Xcode_13.2.1.app'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v2 uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: 'zulu'
java-version: ${{ env.JAVA_VERSION }} java-version: ${{ env.JAVA_VERSION }}
architecture: ${{ matrix.architecture }}
cache: 'maven' cache: 'maven'
- id: versions - id: versions
name: Apply version information name: Apply version information
run: | run: |
if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
@@ -186,7 +199,7 @@ jobs:
--icon ".background" 128 758 --icon ".background" 128 758
--icon ".fseventsd" 320 758 --icon ".fseventsd" 320 758
--icon ".VolumeIcon.icns" 512 758 --icon ".VolumeIcon.icns" 512 758
Cryptomator-${VERSION_NO}.dmg dmg Cryptomator-${VERSION_NO}-${{ matrix.output-suffix }}.dmg dmg
env: env:
VERSION_NO: ${{ steps.versions.outputs.semVerNum }} VERSION_NO: ${{ steps.versions.outputs.semVerNum }}
- name: Notarize .dmg - name: Notarize .dmg
@@ -197,8 +210,9 @@ jobs:
apple-id: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }} apple-id: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
password: ${{ secrets.MACOS_NOTARIZATION_PW }} password: ${{ secrets.MACOS_NOTARIZATION_PW }}
team-id: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }} team-id: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
xcode-path: ${{ matrix.xcode-path }}
- name: Add possible alpha/beta tags to installer name - name: Add possible alpha/beta tags to installer name
run: mv Cryptomator-*.dmg Cryptomator-${{ steps.versions.outputs.semVerStr }}.dmg run: mv Cryptomator-*.dmg Cryptomator-${{ steps.versions.outputs.semVerStr }}-${{ matrix.output-suffix }}.dmg
- name: Create detached GPG signature with key 615D449FE6E6A235 - name: Create detached GPG signature with key 615D449FE6E6A235
run: | run: |
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
@@ -213,7 +227,7 @@ jobs:
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: dmg name: dmg-${{ matrix.output-suffix }}
path: Cryptomator-*.dmg path: Cryptomator-*.dmg
if-no-files-found: error if-no-files-found: error
- name: Publish dmg on GitHub Releases - name: Publish dmg on GitHub Releases
@@ -225,5 +239,3 @@ jobs:
files: | files: |
Cryptomator-*.dmg Cryptomator-*.dmg
Cryptomator-*.asc Cryptomator-*.asc
+4 -4
View File
@@ -4,7 +4,7 @@ on:
pull_request: pull_request:
env: env:
JAVA_VERSION: 17 JAVA_VERSION: 19
defaults: defaults:
run: run:
@@ -16,10 +16,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-java@v2 - uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: 'zulu'
java-version: ${{ env.JAVA_VERSION }} java-version: ${{ env.JAVA_VERSION }}
cache: 'maven' cache: 'maven'
- name: Build and Test - name: Build and Test
+43
View File
@@ -0,0 +1,43 @@
name: Release Check
on:
push:
branches:
- 'release/**'
- 'hotfix/**'
env:
JAVA_VERSION: 19
defaults:
run:
shell: bash
jobs:
release-check-precondition:
name: Validate commits pushed to release/hotfix branch to fulfill release requirements
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- id: validate-pom-version
name: Validate POM version
run: |
if [[ $GITHUB_REF =~ refs/heads/(hotfix|release)/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR=${GITHUB_REF##*/}
else
echo "Failed to parse version"
exit 1
fi
if [[ ${SEM_VER_STR} == `mvn help:evaluate -Dexpression=project.version -q -DforceStdout` ]]; then
echo "::set-output name=semVerStr::${SEM_VER_STR}"
else
echo "Version not set in POM"
exit 1
fi
- name: Validate release in org.cryptomator.Cryptomator.metainfo.xml file
run: |
if ! grep -q "<release date=\".*\" version=\"${{ steps.validate-pom-version.outputs.semVerStr }}\"/>" dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml; then
echo "Release not set in dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml"
exit 1
fi
+18 -27
View File
@@ -8,14 +8,11 @@ on:
version: version:
description: 'Version' description: 'Version'
required: false required: false
winget-release:
description: 'Release artifacts to winget'
required: true
type: boolean
default: false
env: env:
JAVA_VERSION: 17 JAVA_VERSION: 19
JAVA_DIST: 'zulu'
JAVA_CACHE: 'maven'
defaults: defaults:
run: run:
@@ -26,15 +23,15 @@ jobs:
name: Build .msi Installer name: Build .msi Installer
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v2 uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }} java-version: ${{ env.JAVA_VERSION }}
cache: 'maven' cache: ${{ env.JAVA_CACHE }}
- id: versions - id: versions
name: Apply version information name: Apply version information
run: | run: |
@@ -194,38 +191,32 @@ jobs:
semVerStr: ${{ steps.versions.outputs.semVerStr }} semVerStr: ${{ steps.versions.outputs.semVerStr }}
revNum: ${{ steps.versions.outputs.revNum }} revNum: ${{ steps.versions.outputs.revNum }}
publish-winget: call-winget-flow:
name: Publish on winget repo
runs-on: windows-latest
needs: [build-msi] needs: [build-msi]
if: github.event.action == 'release' || inputs.winget-release if: github.event.action == 'published'
steps: uses: ./.github/workflows/winget.yml
- name: Submit package to Windows Package Manager Community Repository with:
run: | releaseTag: ${{ github.event.release.tag_name }}
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
$github = Get-Content '${{ github.event_path }}' | ConvertFrom-Json
$installerUrl = $github.release.assets | Where-Object -Property name -match 'Cryptomator-*.msi' | Select -ExpandProperty browser_download_url -First 1
.\wingetcreate.exe update Cryptomator.Cryptomator -s -v $github.release.tag_name -u $installerUrl -t ${{ secrets.CRYPTOBOT_WINGET_TOKEN }}
shell: pwsh
build-exe: build-exe:
name: Build .exe installer name: Build .exe installer
runs-on: windows-latest runs-on: windows-latest
needs: [build-msi] needs: [build-msi]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Download .msi - name: Download .msi
uses: actions/download-artifact@v2 uses: actions/download-artifact@v3
with: with:
name: msi name: msi
path: dist/win/bundle/resources path: dist/win/bundle/resources
- name: Strip version info from msi file name - name: Strip version info from msi file name
run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi
- uses: actions/setup-java@v2 - uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }} java-version: ${{ env.JAVA_VERSION }}
cache: 'maven' cache: ${{ env.JAVA_CACHE }}
- name: Generate license for exe - name: Generate license for exe
run: > run: >
mvn -B license:add-third-party mvn -B license:add-third-party
+49
View File
@@ -0,0 +1,49 @@
name: Release to Winget
on:
workflow_call:
inputs:
releaseTag:
required: true
type: string
workflow_dispatch:
inputs:
releaseTag:
description: 'Release tag name'
required: true
type: string
jobs:
publish-winget:
name: Publish on winget repo
runs-on: windows-latest
steps:
- name: Get download url for msi artifacts
id: get-release-assets
uses: actions/github-script@v6
with:
script: |
const query =`query($tag:String!) {
repository(owner:"cryptomator", name:"cryptomator"){
release(tagName: $tag) {
releaseAssets(first:20) {
nodes {
name
downloadUrl
}
}
}
}
}`;
const variables = {
tag: "${{ inputs.releaseTag }}"
}
return await github.graphql(query, variables)
- name: Submit package to Windows Package Manager Community Repository
id: submit-winget
run: |
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
$releaseAssets = (ConvertFrom-Json '${{ steps.get-release-assets.outputs.result }}').repository.release.releaseAssets.nodes
$installerUrl = $releaseAssets | Where-Object -Property name -match '^Cryptomator-.*\.msi$' | Select -ExpandProperty downloadUrl -First 1
.\wingetcreate.exe update Cryptomator.Cryptomator -s -v "${{ inputs.releaseTag }}" -u "$installerUrl" -t ${{ secrets.CRYPTOBOT_WINGET_TOKEN }}
shell: pwsh
+11 -11
View File
@@ -14,10 +14,10 @@
<option name="dagger.fastInit" value="enabled" /> <option name="dagger.fastInit" value="enabled" />
<option name="dagger.formatGeneratedSource" value="enabled" /> <option name="dagger.formatGeneratedSource" value="enabled" />
<processorPath useClasspath="false"> <processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.41/dagger-compiler-2.41.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.44/dagger-compiler-2.44.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.41/dagger-2.41.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.44/dagger-2.44.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" /> <entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.41/dagger-producers-2.41.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.44/dagger-producers-2.44.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/31.0.1-jre/guava-31.0.1-jre.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/31.0.1-jre/guava-31.0.1-jre.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
@@ -26,18 +26,18 @@
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.7.1/error_prone_annotations-2.7.1.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.7.1/error_prone_annotations-2.7.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.5/checker-compat-qual-2.5.5.jar" /> <entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.5/checker-compat-qual-2.5.5.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.41/dagger-spi-2.41.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.44/dagger-spi-2.44.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/devtools/ksp/symbol-processing-api/1.5.30-1.0.0/symbol-processing-api-1.5.30-1.0.0.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/devtools/ksp/symbol-processing-api/1.7.0-1.0.6/symbol-processing-api-1.7.0-1.0.6.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.5.32/kotlin-stdlib-jdk8-1.5.32.jar" /> <entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.7.0/kotlin-stdlib-1.7.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.5.32/kotlin-stdlib-1.5.32.jar" /> <entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.7.0/kotlin-stdlib-common-1.7.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" /> <entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.5.32/kotlin-stdlib-common-1.5.32.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.5.32/kotlin-stdlib-jdk7-1.5.32.jar" />
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.13.0/javapoet-1.13.0.jar" /> <entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.13.0/javapoet-1.13.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.7.0/kotlin-stdlib-jdk8-1.7.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.7.0/kotlin-stdlib-jdk7-1.7.0.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/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$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
<entry name="$MAVEN_REPOSITORY$/net/ltgt/gradle/incap/incap/0.2/incap-0.2.jar" /> <entry name="$MAVEN_REPOSITORY$/net/ltgt/gradle/incap/incap/0.2/incap-0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.3.0/kotlinx-metadata-jvm-0.3.0.jar" /> <entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.5.0/kotlinx-metadata-jvm-0.5.0.jar" />
</processorPath> </processorPath>
<module name="cryptomator" /> <module name="cryptomator" />
</profile> </profile>
@@ -45,7 +45,7 @@
</component> </component>
<component name="JavacSettings"> <component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE"> <option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="cryptomator" options="-Adagger.fastInit=enabled -Adagger.formatGeneratedSource=enabled" /> <module name="cryptomator" options="-Adagger.fastInit=enabled -Adagger.formatGeneratedSource=enabled --enable-preview" />
</option> </option>
</component> </component>
</project> </project>
+1 -1
View File
@@ -8,7 +8,7 @@
</list> </list>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_19" default="true" project-jdk-name="19" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application"> <configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" /> <module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.p12Path=&quot;~/.config/Cryptomator/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" /> <option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.p12Path=&quot;~/.config/Cryptomator/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application"> <configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" /> <module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.p12Path=&quot;~/.config/Cryptomator-Dev/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator-Dev/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dcryptomator.showTrayIcon=true -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m" /> <option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.p12Path=&quot;~/.config/Cryptomator-Dev/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator-Dev/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dcryptomator.showTrayIcon=true -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application"> <configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" /> <module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator/Plugins&quot; -Dcryptomator.integrationsWin.keychainPaths=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Dcryptomator.p12Path=&quot;~/AppData/Roaming/Cryptomator/key.p12&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" /> <option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator/Plugins&quot; -Dcryptomator.integrationsWin.keychainPaths=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Dcryptomator.p12Path=&quot;~/AppData/Roaming/Cryptomator/key.p12&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.win" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application"> <configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" /> <module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator-Dev/Plugins&quot; -Dcryptomator.integrationsWin.keychainPaths=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.p12Path=&quot;~/AppData/Roaming/Cryptomator-Dev/key.p12&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" /> <option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator-Dev/Plugins&quot; -Dcryptomator.integrationsWin.keychainPaths=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.p12Path=&quot;~/AppData/Roaming/Cryptomator-Dev/key.p12&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.win" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+1 -1
View File
@@ -5,7 +5,7 @@
</envs> </envs>
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" /> <module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.p12Path=&quot;~/Library/Application Support/Cryptomator/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator/Plugins&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" /> <option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.p12Path=&quot;~/Library/Application Support/Cryptomator/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator/Plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+1 -1
View File
@@ -5,7 +5,7 @@
</envs> </envs>
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" /> <module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.p12Path=&quot;~/Library/Application Support/Cryptomator-Dev/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator-Dev/Plugins&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" /> <option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.p12Path=&quot;~/Library/Application Support/Cryptomator-Dev/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator-Dev/Plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+8 -1
View File
@@ -32,10 +32,17 @@ Cryptomator is provided free of charge as an open-source project despite the hig
<tbody> <tbody>
<tr> <tr>
<td><a href="https://mowcapital.com/"><img src="https://cryptomator.org/img/sponsors/mowcapital.svg" alt="Mow Capital" height="40"></a></td> <td><a href="https://mowcapital.com/"><img src="https://cryptomator.org/img/sponsors/mowcapital.svg" alt="Mow Capital" height="40"></a></td>
<td><a href="https://www.easeus.com/"><img src="https://cryptomator.org/img/sponsors/easeus.png" alt="EaseUS" height="40"></a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
### Special Shoutout
Continuous integration hosting for ARM64 builds is provided by [MacStadium](https://www.macstadium.com/opensource).
<a href="https://www.macstadium.com/opensource"><img src="https://uploads-ssl.webflow.com/5ac3c046c82724970fc60918/5c019d917bba312af7553b49_MacStadium-developerlogo.png" alt="MacStadium" height="100"></a>
--- ---
## Introduction ## Introduction
@@ -85,7 +92,7 @@ For more information on the security details visit [cryptomator.org](https://doc
``` ```
mvn clean install mvn clean install
# or mvn clean install -Pwindows # or mvn clean install -Pwin
# or mvn clean install -Pmac # or mvn clean install -Pmac
# or mvn clean install -Plinux # or mvn clean install -Plinux
``` ```
+2 -2
View File
@@ -19,7 +19,7 @@ fi
export LD_PRELOAD=lib/app/libjffi.so export LD_PRELOAD=lib/app/libjffi.so
if [ "$GTK2_PRESENT" -eq 0 ] && [ "$GTK3_PRESENT" -ne 0 ]; then if [ "$GTK2_PRESENT" -eq 0 ] && [ "$GTK3_PRESENT" -ne 0 ]; then
bin/Cryptomator-gtk2 bin/Cryptomator-gtk2 $@
else else
bin/Cryptomator bin/Cryptomator $@
fi fi
@@ -66,6 +66,8 @@
</content_rating> </content_rating>
<releases> <releases>
<release date="2022-10-06" version="1.6.15"/>
<release date="2022-08-31" version="1.6.14"/>
<release date="2022-07-27" version="1.6.12"/> <release date="2022-07-27" version="1.6.12"/>
<release date="2022-07-26" version="1.6.11"/> <release date="2022-07-26" version="1.6.11"/>
<release date="2022-05-03" version="1.6.10"/> <release date="2022-05-03" version="1.6.10"/>
+1 -1
View File
@@ -1,4 +1,4 @@
cryptomator (${PPA_VERSION}) bionic; urgency=low cryptomator (${PPA_VERSION}) focal; urgency=low
* Full changelog can be found on https://github.com/cryptomator/cryptomator/releases * Full changelog can be found on https://github.com/cryptomator/cryptomator/releases
+1 -1
View File
@@ -2,7 +2,7 @@ Source: cryptomator
Maintainer: Cryptobot <releases@cryptomator.org> Maintainer: Cryptobot <releases@cryptomator.org>
Section: utils Section: utils
Priority: optional Priority: optional
Build-Depends: debhelper (>=10), openjdk-17-jdk Build-Depends: debhelper (>=10), coffeelibs-jdk-19
Standards-Version: 4.5.0 Standards-Version: 4.5.0
Homepage: https://cryptomator.org Homepage: https://cryptomator.org
Vcs-Git: https://github.com/cryptomator/cryptomator.git Vcs-Git: https://github.com/cryptomator/cryptomator.git
+1 -1
View File
@@ -3,4 +3,4 @@
# fix for https://github.com/cryptomator/cryptomator/issues/1370 # fix for https://github.com/cryptomator/cryptomator/issues/1370
export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/jni/libjffi-1.2.so export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/jni/libjffi-1.2.so
/usr/lib/cryptomator/bin/cryptomator /usr/lib/cryptomator/bin/cryptomator $@
+4 -2
View File
@@ -4,6 +4,8 @@
# Uncomment this to turn on verbose mode. # Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1 #export DH_VERBOSE=1
JAVA_HOME = /usr/lib/jvm/java-19-coffeelibs
%: %:
dh $@ dh $@
@@ -16,7 +18,7 @@ override_dh_auto_clean:
override_dh_auto_build: override_dh_auto_build:
mkdir resources mkdir resources
ln -s ../common/org.cryptomator.Cryptomator512.png resources/cryptomator.png ln -s ../common/org.cryptomator.Cryptomator512.png resources/cryptomator.png
jlink \ $(JAVA_HOME)/bin/jlink \
--output runtime \ --output runtime \
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \ --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--strip-native-commands \ --strip-native-commands \
@@ -24,7 +26,7 @@ override_dh_auto_build:
--no-man-pages \ --no-man-pages \
--strip-debug \ --strip-debug \
--compress=2 --compress=2
jpackage \ $(JAVA_HOME)/bin/jpackage \
--type app-image \ --type app-image \
--runtime-image runtime \ --runtime-image runtime \
--input libs \ --input libs \
+1 -1
View File
@@ -1,2 +1,2 @@
[InternetShortcut] [InternetShortcut]
URL=https://github.com/winfsp/winfsp/releases/download/v1.11/winfsp-1.11.22176.msi URL=https://github.com/winfsp/winfsp/releases/download/v1.12/winfsp-1.12.22301.msi
+26 -24
View File
@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>cryptomator</artifactId> <artifactId>cryptomator</artifactId>
<version>1.6.13</version> <version>1.7.0</version>
<name>Cryptomator Desktop App</name> <name>Cryptomator Desktop App</name>
<organization> <organization>
@@ -21,45 +21,46 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.jdk.version>17</project.jdk.version> <project.jdk.version>19</project.jdk.version>
<!-- Group IDs of jars that need to stay on the class path for now --> <!-- Group IDs of jars that need to stay on the class path for now -->
<nonModularGroupIds>com.github.serceman,com.github.jnr,org.ow2.asm,net.java.dev.jna,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh</nonModularGroupIds> <!-- Check progress on https://github.com/swiesend/secret-service/issues/31 to remove hypfvieh, swiesend and jnr -->
<nonModularGroupIds>com.github.jnr,org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh</nonModularGroupIds>
<!-- cryptomator dependencies --> <!-- cryptomator dependencies -->
<cryptomator.cryptolib.version>2.1.0-beta3</cryptomator.cryptolib.version> <cryptomator.cryptolib.version>2.1.0-rc1</cryptomator.cryptolib.version>
<cryptomator.cryptofs.version>2.4.2</cryptomator.cryptofs.version> <cryptomator.cryptofs.version>2.4.5</cryptomator.cryptofs.version>
<cryptomator.integrations.version>1.1.0</cryptomator.integrations.version> <cryptomator.integrations.version>1.2.0-beta2</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.1.2</cryptomator.integrations.win.version> <cryptomator.integrations.win.version>1.1.2</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.1.2</cryptomator.integrations.mac.version> <cryptomator.integrations.mac.version>1.1.2</cryptomator.integrations.mac.version>
<cryptomator.integrations.linux.version>1.1.0</cryptomator.integrations.linux.version> <cryptomator.integrations.linux.version>1.1.0</cryptomator.integrations.linux.version>
<cryptomator.fuse.version>1.3.4</cryptomator.fuse.version> <cryptomator.fuse.version>2.0.0-beta1</cryptomator.fuse.version>
<cryptomator.dokany.version>1.3.3</cryptomator.dokany.version> <cryptomator.dokany.version>2.0.0-beta1</cryptomator.dokany.version>
<cryptomator.webdav.version>1.2.8</cryptomator.webdav.version> <cryptomator.webdav.version>2.0.0-beta1</cryptomator.webdav.version>
<!-- 3rd party dependencies --> <!-- 3rd party dependencies -->
<commons-lang3.version>3.12.0</commons-lang3.version> <commons-lang3.version>3.12.0</commons-lang3.version>
<dagger.version>2.41</dagger.version> <dagger.version>2.44</dagger.version>
<easybind.version>2.2</easybind.version> <easybind.version>2.2</easybind.version>
<guava.version>31.1-jre</guava.version> <guava.version>31.1-jre</guava.version>
<gson.version>2.9.0</gson.version> <gson.version>2.9.1</gson.version>
<javafx.version>18.0.1</javafx.version> <javafx.version>19</javafx.version>
<jwt.version>4.0.0</jwt.version> <jwt.version>4.2.1</jwt.version>
<nimbus-jose.version>9.23</nimbus-jose.version> <nimbus-jose.version>9.25.4</nimbus-jose.version>
<logback.version>1.2.11</logback.version> <logback.version>1.4.4</logback.version>
<slf4j.version>1.7.36</slf4j.version> <slf4j.version>2.0.3</slf4j.version>
<tinyoauth2.version>0.5.1</tinyoauth2.version> <tinyoauth2.version>0.5.1</tinyoauth2.version>
<zxcvbn.version>1.7.0</zxcvbn.version> <zxcvbn.version>1.7.0</zxcvbn.version>
<!-- test dependencies --> <!-- test dependencies -->
<junit.jupiter.version>5.8.1</junit.jupiter.version> <junit.jupiter.version>5.9.1</junit.jupiter.version>
<mockito.version>4.4.0</mockito.version> <mockito.version>4.8.0</mockito.version>
<hamcrest.version>2.2</hamcrest.version> <hamcrest.version>2.2</hamcrest.version>
<!-- build-time dependencies --> <!-- build-time dependencies -->
<jetbrains.annotations.version>23.0.0</jetbrains.annotations.version> <jetbrains.annotations.version>23.0.0</jetbrains.annotations.version>
<dependency-check.version>7.1.0</dependency-check.version> <dependency-check.version>7.2.1</dependency-check.version>
<jacoco.version>0.8.7</jacoco.version> <jacoco.version>0.8.8</jacoco.version>
</properties> </properties>
<dependencies> <dependencies>
@@ -263,7 +264,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId> <artifactId>maven-resources-plugin</artifactId>
<version>3.2.0</version> <version>3.3.0</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
@@ -273,7 +274,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version> <version>3.0.0-M7</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
@@ -283,7 +284,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version> <version>3.3.0</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.jacoco</groupId> <groupId>org.jacoco</groupId>
@@ -313,13 +314,14 @@
<compilerArgs> <compilerArgs>
<arg>-Adagger.fastInit=enabled</arg> <arg>-Adagger.fastInit=enabled</arg>
<arg>-Adagger.formatGeneratedSource=enabled</arg> <arg>-Adagger.formatGeneratedSource=enabled</arg>
<arg>--enable-preview</arg>
</compilerArgs> </compilerArgs>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId> <artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version> <version>3.1.0</version>
<executions> <executions>
<execution> <execution>
<id>compile-light-theme</id> <id>compile-light-theme</id>
+7 -35
View File
@@ -1,11 +1,12 @@
import ch.qos.logback.classic.spi.Configurator;
import org.cryptomator.integrations.tray.TrayMenuController; import org.cryptomator.integrations.tray.TrayMenuController;
import org.cryptomator.logging.LogbackConfiguratorFactory;
import org.cryptomator.ui.traymenu.AwtTrayMenuController; import org.cryptomator.ui.traymenu.AwtTrayMenuController;
module org.cryptomator.desktop { open module org.cryptomator.desktop {
requires static org.jetbrains.annotations; requires static org.jetbrains.annotations;
requires org.cryptomator.cryptolib; requires org.cryptomator.cryptolib;
requires org.cryptomator.cryptofs; requires org.cryptomator.cryptofs;
requires org.cryptomator.frontend.dokany; requires org.cryptomator.frontend.dokany;
requires org.cryptomator.frontend.fuse; requires org.cryptomator.frontend.fuse;
@@ -20,9 +21,12 @@ module org.cryptomator.desktop {
requires javafx.fxml; requires javafx.fxml;
requires jdk.crypto.ec; requires jdk.crypto.ec;
// 3rd party: // 3rd party:
requires ch.qos.logback.classic;
requires ch.qos.logback.core;
requires com.auth0.jwt; requires com.auth0.jwt;
requires com.google.common; requires com.google.common;
requires com.google.gson; requires com.google.gson;
requires com.nimbusds.jose.jwt;
requires com.nulabinc.zxcvbn; requires com.nulabinc.zxcvbn;
requires com.tobiasdiez.easybind; requires com.tobiasdiez.easybind;
requires dagger; requires dagger;
@@ -32,39 +36,7 @@ module org.cryptomator.desktop {
/* TODO: filename-based modules: */ /* TODO: filename-based modules: */
requires static javax.inject; /* ugly dagger/guava crap */ requires static javax.inject; /* ugly dagger/guava crap */
requires logback.classic;
requires logback.core;
requires com.nimbusds.jose.jwt;
exports org.cryptomator.ui.traymenu to org.cryptomator.integrations.api;
provides TrayMenuController with AwtTrayMenuController; provides TrayMenuController with AwtTrayMenuController;
provides Configurator with LogbackConfiguratorFactory;
exports org.cryptomator.ui.keyloading.hub to com.fasterxml.jackson.databind;
opens org.cryptomator.common.settings to com.google.gson;
opens org.cryptomator.ui.keyloading.hub to com.google.gson, javafx.fxml;
opens org.cryptomator.launcher to javafx.graphics;
opens org.cryptomator.common to javafx.fxml;
opens org.cryptomator.common.vaults to javafx.fxml;
opens org.cryptomator.ui.addvaultwizard to javafx.fxml;
opens org.cryptomator.ui.changepassword to javafx.fxml;
opens org.cryptomator.ui.common to javafx.fxml;
opens org.cryptomator.ui.controls to javafx.fxml;
opens org.cryptomator.ui.forgetPassword to javafx.fxml;
opens org.cryptomator.ui.fxapp to javafx.fxml;
opens org.cryptomator.ui.health to javafx.fxml;
opens org.cryptomator.ui.keyloading.masterkeyfile to javafx.fxml;
opens org.cryptomator.ui.lock to javafx.fxml;
opens org.cryptomator.ui.mainwindow to javafx.fxml;
opens org.cryptomator.ui.migration to javafx.fxml;
opens org.cryptomator.ui.preferences to javafx.fxml;
opens org.cryptomator.ui.quit to javafx.fxml;
opens org.cryptomator.ui.recoverykey to javafx.fxml;
opens org.cryptomator.ui.removevault to javafx.fxml;
opens org.cryptomator.ui.stats to javafx.fxml;
opens org.cryptomator.ui.unlock to javafx.fxml;
opens org.cryptomator.ui.vaultoptions to javafx.fxml;
opens org.cryptomator.ui.wrongfilealert to javafx.fxml;
} }
@@ -10,6 +10,7 @@ import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
@@ -28,6 +29,18 @@ public final class CatchingExecutors {
super(corePoolSize, threadFactory); super(corePoolSize, threadFactory);
} }
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
Runnable oneShot = () -> this.execute(command);
return super.scheduleAtFixedRate(oneShot, initialDelay, period, unit);
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
Runnable oneShot = () -> this.execute(command);
return super.scheduleWithFixedDelay(oneShot, initialDelay, delay, unit);
}
@Override @Override
protected void afterExecute(Runnable runnable, Throwable throwable) { protected void afterExecute(Runnable runnable, Throwable throwable) {
super.afterExecute(runnable, throwable); super.afterExecute(runnable, throwable);
@@ -77,6 +90,12 @@ public final class CatchingExecutors {
} }
private static void afterExecuteFuture(Future<?> future) { private static void afterExecuteFuture(Future<?> future) {
if (future instanceof ScheduledFuture<?> && !future.isDone()) {
//we assume that this must be a repeated ScheduledFutureTask, where the done-status is only set when not executed anymore
//see also https://github.com/cryptomator/cryptomator/pull/2422
return;
}
try { try {
future.get(); future.get();
} catch (CancellationException ce) { } catch (CancellationException ce) {
@@ -10,19 +10,18 @@ import dagger.Module;
import dagger.Provides; import dagger.Provides;
import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.keychain.KeychainModule; import org.cryptomator.common.keychain.KeychainModule;
import org.cryptomator.common.mount.MountModule;
import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.SettingsProvider; import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.common.vaults.VaultComponent; import org.cryptomator.common.vaults.VaultComponent;
import org.cryptomator.common.vaults.VaultListModule; import org.cryptomator.common.vaults.VaultListModule;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import javafx.beans.binding.Binding; import javafx.beans.value.ObservableValue;
import javafx.beans.binding.Bindings;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
@@ -33,7 +32,7 @@ import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class}) @Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class, MountModule.class})
public abstract class CommonsModule { public abstract class CommonsModule {
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class); private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
@@ -41,6 +40,12 @@ public abstract class CommonsModule {
private static final int NUM_CORE_BG_THREADS = 6; private static final int NUM_CORE_BG_THREADS = 6;
private static final long BG_THREAD_KEEPALIVE_SECONDS = 60l; private static final long BG_THREAD_KEEPALIVE_SECONDS = 60l;
@Provides
@Singleton
static Environment provideEnvironment() {
return Environment.getInstance();
}
@SuppressWarnings("SpellCheckingInspection") @SuppressWarnings("SpellCheckingInspection")
@Provides @Provides
@Singleton @Singleton
@@ -124,20 +129,11 @@ public abstract class CommonsModule {
@Provides @Provides
@Singleton @Singleton
static Binding<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) { static ObservableValue<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
return Bindings.createObjectBinding(() -> { return settings.port().map(port -> {
String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost"; String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
return InetSocketAddress.createUnresolved(host, settings.port().intValue()); return InetSocketAddress.createUnresolved(host, settings.port().intValue());
}, settings.port()); });
}
@Provides
@Singleton
static WebDavServer provideWebDavServer(Binding<InetSocketAddress> serverSocketAddressBinding) {
WebDavServer server = WebDavServer.create();
// no need to unsubscribe eventually, because server is a singleton
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
return server;
} }
} }
@@ -5,8 +5,6 @@ import com.google.common.base.Strings;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@@ -17,7 +15,6 @@ import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
@Singleton
public class Environment { public class Environment {
private static final Logger LOG = LoggerFactory.getLogger(Environment.class); private static final Logger LOG = LoggerFactory.getLogger(Environment.class);
@@ -36,24 +33,37 @@ public class Environment {
private static final String PLUGIN_DIR_PROP_NAME = "cryptomator.pluginDir"; private static final String PLUGIN_DIR_PROP_NAME = "cryptomator.pluginDir";
private static final String TRAY_ICON_PROP_NAME = "cryptomator.showTrayIcon"; private static final String TRAY_ICON_PROP_NAME = "cryptomator.showTrayIcon";
@Inject private Environment() {}
public Environment() {
LOG.debug("user.home: {}", System.getProperty("user.home")); public void log() {
LOG.debug("java.library.path: {}", System.getProperty("java.library.path")); LOG.info("user.home: {}", System.getProperty("user.home"));
LOG.debug("user.language: {}", System.getProperty("user.language")); LOG.info("java.library.path: {}", System.getProperty("java.library.path"));
LOG.debug("user.region: {}", System.getProperty("user.region")); LOG.info("user.language: {}", System.getProperty("user.language"));
LOG.debug("logback.configurationFile: {}", System.getProperty("logback.configurationFile")); LOG.info("user.region: {}", System.getProperty("user.region"));
LOG.debug("{}: {}", SETTINGS_PATH_PROP_NAME, System.getProperty(SETTINGS_PATH_PROP_NAME)); LOG.info("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));
LOG.debug("{}: {}", IPC_SOCKET_PATH_PROP_NAME, System.getProperty(IPC_SOCKET_PATH_PROP_NAME)); logCryptomatorSystemProperty(SETTINGS_PATH_PROP_NAME);
LOG.debug("{}: {}", KEYCHAIN_PATHS_PROP_NAME, System.getProperty(KEYCHAIN_PATHS_PROP_NAME)); logCryptomatorSystemProperty(IPC_SOCKET_PATH_PROP_NAME);
LOG.debug("{}: {}", LOG_DIR_PROP_NAME, System.getProperty(LOG_DIR_PROP_NAME)); logCryptomatorSystemProperty(KEYCHAIN_PATHS_PROP_NAME);
LOG.debug("{}: {}", PLUGIN_DIR_PROP_NAME, System.getProperty(PLUGIN_DIR_PROP_NAME)); logCryptomatorSystemProperty(LOG_DIR_PROP_NAME);
LOG.debug("{}: {}", MOUNTPOINT_DIR_PROP_NAME, System.getProperty(MOUNTPOINT_DIR_PROP_NAME)); logCryptomatorSystemProperty(PLUGIN_DIR_PROP_NAME);
LOG.debug("{}: {}", MIN_PW_LENGTH_PROP_NAME, System.getProperty(MIN_PW_LENGTH_PROP_NAME)); logCryptomatorSystemProperty(MOUNTPOINT_DIR_PROP_NAME);
LOG.debug("{}: {}", APP_VERSION_PROP_NAME, System.getProperty(APP_VERSION_PROP_NAME)); logCryptomatorSystemProperty(MIN_PW_LENGTH_PROP_NAME);
LOG.debug("{}: {}", BUILD_NUMBER_PROP_NAME, System.getProperty(BUILD_NUMBER_PROP_NAME)); logCryptomatorSystemProperty(APP_VERSION_PROP_NAME);
LOG.debug("{}: {}", TRAY_ICON_PROP_NAME, System.getProperty(TRAY_ICON_PROP_NAME)); logCryptomatorSystemProperty(BUILD_NUMBER_PROP_NAME);
LOG.debug("{}: {}", P12_PATH_PROP_NAME, System.getProperty(P12_PATH_PROP_NAME)); logCryptomatorSystemProperty(TRAY_ICON_PROP_NAME);
logCryptomatorSystemProperty(P12_PATH_PROP_NAME);
}
public static Environment getInstance() {
final class Holder {
private static final Environment INSTANCE = new Environment();
}
return Holder.INSTANCE;
}
private void logCryptomatorSystemProperty(String propertyName) {
LOG.info("{}: {}", propertyName, System.getProperty(propertyName));
} }
public boolean useCustomLogbackConfig() { public boolean useCustomLogbackConfig() {
@@ -88,8 +98,13 @@ public class Environment {
return getPath(MOUNTPOINT_DIR_PROP_NAME).map(this::replaceHomeDir); return getPath(MOUNTPOINT_DIR_PROP_NAME).map(this::replaceHomeDir);
} }
public Optional<String> getAppVersion() { /**
return Optional.ofNullable(System.getProperty(APP_VERSION_PROP_NAME)); * Returns the app version defined in the {@value APP_VERSION_PROP_NAME} property or returns "SNAPSHOT".
*
* @return App version or "SNAPSHOT", if undefined
*/
public String getAppVersion() {
return System.getProperty(APP_VERSION_PROP_NAME, "SNAPSHOT");
} }
public Optional<String> getBuildNumber() { public Optional<String> getBuildNumber() {
@@ -10,6 +10,7 @@ import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding; import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import java.util.Optional; import java.util.Optional;
@Singleton @Singleton
@@ -18,7 +19,7 @@ public class LicenseHolder {
private final Settings settings; private final Settings settings;
private final LicenseChecker licenseChecker; private final LicenseChecker licenseChecker;
private final ObjectProperty<DecodedJWT> validJwtClaims; private final ObjectProperty<DecodedJWT> validJwtClaims;
private final StringBinding licenseSubject; private final ObservableValue<String> licenseSubject;
private final BooleanBinding validLicenseProperty; private final BooleanBinding validLicenseProperty;
@Inject @Inject
@@ -26,7 +27,7 @@ public class LicenseHolder {
this.settings = settings; this.settings = settings;
this.licenseChecker = licenseChecker; this.licenseChecker = licenseChecker;
this.validJwtClaims = new SimpleObjectProperty<>(); this.validJwtClaims = new SimpleObjectProperty<>();
this.licenseSubject = Bindings.createStringBinding(this::getLicenseSubject, validJwtClaims); this.licenseSubject = validJwtClaims.map(DecodedJWT::getSubject);
this.validLicenseProperty = validJwtClaims.isNotNull(); this.validLicenseProperty = validJwtClaims.isNotNull();
Optional<DecodedJWT> claims = licenseChecker.check(settings.licenseKey().get()); Optional<DecodedJWT> claims = licenseChecker.check(settings.licenseKey().get());
@@ -55,17 +56,12 @@ public class LicenseHolder {
} }
} }
public StringBinding licenseSubjectProperty() { public ObservableValue<String> licenseSubjectProperty() {
return licenseSubject; return licenseSubject;
} }
public String getLicenseSubject() { public String getLicenseSubject() {
DecodedJWT claims = validJwtClaims.get(); return licenseSubject.getValue();
if (claims != null) {
return claims.getSubject();
} else {
return null;
}
} }
public BooleanBinding validLicenseProperty() { public BooleanBinding validLicenseProperty() {
@@ -23,11 +23,14 @@ public class KeychainModule {
@Singleton @Singleton
static ObjectExpression<KeychainAccessProvider> provideKeychainAccessProvider(Settings settings, List<KeychainAccessProvider> providers) { static ObjectExpression<KeychainAccessProvider> provideKeychainAccessProvider(Settings settings, List<KeychainAccessProvider> providers) {
return Bindings.createObjectBinding(() -> { return Bindings.createObjectBinding(() -> {
if (!settings.useKeychain().get()) {
return null;
}
var selectedProviderClass = settings.keychainProvider().get(); var selectedProviderClass = settings.keychainProvider().get();
var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny(); var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny();
var fallbackProvider = providers.stream().findFirst().orElse(null); var fallbackProvider = providers.stream().findFirst().orElse(null);
return selectedProvider.orElse(fallbackProvider); return selectedProvider.orElse(fallbackProvider);
}, settings.keychainProvider()); }, settings.keychainProvider(), settings.useKeychain());
} }
} }
@@ -0,0 +1,30 @@
package org.cryptomator.common.mount;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.integrations.mount.MountService;
import javax.inject.Singleton;
import javafx.beans.value.ObservableValue;
import java.util.List;
@Module
public class MountModule {
@Provides
@Singleton
static List<MountService> provideSupportedMountServices() {
return MountService.get().toList();
}
@Provides
@Singleton
static ObservableValue<MountService> provideMountService(Settings settings, List<MountService> serviceImpls) {
return settings.mountService().map(desiredServiceImpl -> {
var fallbackProvider = serviceImpls.stream().findFirst().orElse(null);
return serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(desiredServiceImpl)).findAny().orElse(fallbackProvider);
});
}
}
@@ -0,0 +1,70 @@
/*******************************************************************************
* 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.common.mount;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.SystemUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;
@Singleton
public final class WindowsDriveLetters {
private static final Set<Path> A_TO_Z;
static {
var sortedSet = new TreeSet<Path>();
IntStream.rangeClosed('A', 'Z').mapToObj(i -> Path.of((char) i + ":\\")).forEach(sortedSet::add);
A_TO_Z = Collections.unmodifiableSet(sortedSet);
}
@Inject
public WindowsDriveLetters() {
}
public Set<Path> getAll() {
return A_TO_Z;
}
public Set<Path> getOccupied() {
if (!SystemUtils.IS_OS_WINDOWS) {
return Set.of();
} else {
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
return StreamSupport.stream(rootDirs.spliterator(), false).collect(Collectors.toUnmodifiableSet());
}
}
public Set<Path> getAvailable() {
return Sets.difference(getAll(), getOccupied());
}
/**
* Skips A and B and only returns them if all others are occupied.
*
* @return an Optional containing either the letter of a free drive letter or empty, if none is available
*/
public Optional<Path> getFirstDesiredAvailable() {
var availableDriveLetters = getAvailable();
var optString = availableDriveLetters.stream().filter(this::notAOrB).findFirst();
return optString.or(() -> availableDriveLetters.stream().findFirst());
}
private boolean notAOrB(Path driveLetter) {
return !(Path.of("A:\\").equals(driveLetter) || Path.of("B:\\").equals(driveLetter));
}
}
@@ -1,29 +0,0 @@
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;
class AvailableDriveLetterChooser implements MountPointChooser {
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.getDesiredAvailableDriveLetterPath();
}
}
@@ -1,42 +0,0 @@
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.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
class CustomDriveLetterChooser implements MountPointChooser {
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 boolean prepare(Volume caller, Path driveLetter) throws InvalidMountPointException {
if (!Files.notExists(driveLetter, LinkOption.NOFOLLOW_LINKS)) {
//Drive already exists OR can't be determined
throw new InvalidMountPointException(new FileAlreadyExistsException(driveLetter.toString()));
}
return false;
}
}
@@ -1,150 +0,0 @@
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.MountPointRequirement;
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.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Optional;
class CustomMountPointChooser implements MountPointChooser {
private static final String HIDEAWAY_PREFIX = ".~$";
private static final String HIDEAWAY_SUFFIX = ".tmp";
private static final String WIN_HIDDEN = "dos:hidden";
private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class);
private final VaultSettings vaultSettings;
@Inject
public CustomMountPointChooser(VaultSettings vaultSettings) {
this.vaultSettings = vaultSettings;
}
@Override
public boolean isApplicable(Volume caller) {
return caller.getImplementationType() != VolumeImpl.WEBDAV;
}
@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 {
return switch (caller.getMountPointRequirement()) {
case PARENT_NO_MOUNT_POINT -> {
prepareParentNoMountPoint(mountPoint);
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
yield true;
}
case EMPTY_MOUNT_POINT -> {
prepareEmptyMountPoint(mountPoint);
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
yield false;
}
case NONE, UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT -> {
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
}
};
}
//This is case on Windows when using FUSE
//See https://github.com/billziss-gh/winfsp/issues/320
void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException {
Path hideaway = getHideaway(mountPoint);
var mpExists = Files.exists(mountPoint, LinkOption.NOFOLLOW_LINKS);
var hideExists = Files.exists(hideaway, LinkOption.NOFOLLOW_LINKS);
//TODO: possible improvement by just deleting an _empty_ hideaway
if (mpExists && hideExists) { //both resources exist (whatever type)
throw new InvalidMountPointException(new FileAlreadyExistsException(hideaway.toString()));
} else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist
throw new InvalidMountPointException(new NoSuchFileException(mountPoint.toString()));
} else if (!mpExists) { //only hideaway exists
checkIsDirectory(hideaway);
LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint);
try {
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS);
}
} catch (IOException e) {
throw new InvalidMountPointException(e);
}
} else { //only mountpoint exists
try {
checkIsDirectory(mountPoint);
checkIsEmpty(mountPoint);
Files.move(mountPoint, hideaway);
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS);
}
} catch (IOException e) {
throw new InvalidMountPointException(e);
}
}
}
private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException {
//This is the case for Windows when using Dokany and for Linux and Mac
checkIsDirectory(mountPoint);
try {
checkIsEmpty(mountPoint);
} catch (IOException exception) {
throw new InvalidMountPointException("IOException while checking folder content", exception);
}
}
@Override
public void cleanup(Volume caller, Path mountPoint) {
if (caller.getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT) {
Path hideaway = getHideaway(mountPoint);
try {
Files.move(hideaway, mountPoint);
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(mountPoint, WIN_HIDDEN, false);
}
} catch (IOException e) {
LOG.error("Unable to clean up mountpoint {} for Winfsp mounting.", mountPoint, e);
}
}
}
private void checkIsDirectory(Path toCheck) throws InvalidMountPointException {
if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) {
throw new InvalidMountPointException(new NotDirectoryException(toCheck.toString()));
}
}
private void checkIsEmpty(Path toCheck) throws InvalidMountPointException, IOException {
try (var dirStream = Files.list(toCheck)) {
if (dirStream.findFirst().isPresent()) {
throw new InvalidMountPointException(new DirectoryNotEmptyException(toCheck.toString()));
}
}
}
//visible for testing
Path getHideaway(Path mountPoint) {
return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX);
}
}
@@ -1,16 +0,0 @@
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);
}
}
@@ -1,42 +0,0 @@
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.util.Optional;
class MacVolumeMountChooser implements MountPointChooser {
private static final Path VOLUME_PATH = Path.of("/Volumes");
private final VaultSettings vaultSettings;
private final MountPointHelper helper;
@Inject
public MacVolumeMountChooser(VaultSettings vaultSettings, MountPointHelper helper) {
this.vaultSettings = vaultSettings;
this.helper = helper;
}
@Override
public boolean isApplicable(Volume caller) {
return SystemUtils.IS_OS_MAC;
}
@Override
public Optional<Path> chooseMountPoint(Volume caller) {
return Optional.of(helper.chooseTemporaryMountPoint(vaultSettings, VOLUME_PATH));
}
@Override
public boolean prepare(Volume caller, Path mountPoint) {
// 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.
// Therefore we don't need to prepare anything.
return false;
}
}
@@ -1,138 +0,0 @@
package org.cryptomator.common.mountpoint;
import dagger.multibindings.IntKey;
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 IntKey IntKey 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 descending order of their priority
* (higher 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 {
/**
* 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
}
}
@@ -1,64 +0,0 @@
package org.cryptomator.common.mountpoint;
import com.google.common.collect.Iterables;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntKey;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.PerVault;
import javax.inject.Named;
import java.util.Comparator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Dagger-Module for {@link MountPointChooser MountPointChoosers.}<br>
* See there for additional information.
*
* @see MountPointChooser
*/
@Module
public abstract class MountPointChooserModule {
@Binds
@IntoMap
@IntKey(1000)
@PerVault
public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser);
@Binds
@IntoMap
@IntKey(900)
@PerVault
public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser);
@Binds
@IntoMap
@IntKey(800)
@PerVault
public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);
@Binds
@IntoMap
@IntKey(101)
@PerVault
public abstract MountPointChooser bindMacVolumeMountChooser(MacVolumeMountChooser chooser);
@Binds
@IntoMap
@IntKey(100)
@PerVault
public abstract MountPointChooser bindTemporaryMountPointChooser(TemporaryMountPointChooser chooser);
@Provides
@PerVault
@Named("orderedMountPointChoosers")
public static Iterable<MountPointChooser> provideOrderedMountPointChoosers(Map<Integer, MountPointChooser> choosers) {
SortedMap<Integer, MountPointChooser> sortedChoosers = new TreeMap<>(Comparator.reverseOrder());
sortedChoosers.putAll(choosers);
return Iterables.unmodifiableIterable(sortedChoosers.values());
}
}
@@ -1,122 +0,0 @@
package org.cryptomator.common.mountpoint;
import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.VaultSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
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;
import java.util.Optional;
@Singleton
class MountPointHelper {
public static Logger LOG = LoggerFactory.getLogger(MountPointHelper.class);
private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
private final Optional<Path> tmpMountPointDir;
private volatile boolean unmountDebrisCleared = false;
@Inject
public MountPointHelper(Environment env) {
this.tmpMountPointDir = env.getMountPointsDir();
}
public Path chooseTemporaryMountPoint(VaultSettings vaultSettings, Path parentDir) {
String basename = vaultSettings.mountName().get();
//regular
Path mountPoint = parentDir.resolve(basename);
if (Files.notExists(mountPoint)) {
return mountPoint;
}
//with id
mountPoint = parentDir.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 = parentDir.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
if (Files.notExists(mountPoint)) {
return mountPoint;
}
}
LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parentDir, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
return null;
}
public synchronized void clearIrregularUnmountDebrisIfNeeded() {
if (unmountDebrisCleared || tmpMountPointDir.isEmpty()) {
return; // nothing to do
}
if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) {
clearIrregularUnmountDebris(tmpMountPointDir.get());
}
unmountDebrisCleared = true;
}
private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {
IOException cleanupFailed = new IOException("Cleanup failed");
try (var ds = Files.newDirectoryStream(dirContainingMountPoints)) {
LOG.debug("Performing cleanup of mountpoint dir {}.", dirContainingMountPoints);
for (Path p : ds) {
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);
} finally {
unmountDebrisCleared = true;
}
}
private void deleteEmptyDir(Path dir) throws IOException {
assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
try {
ensureIsEmpty(dir);
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 void deleteDeadLink(Path symlink) throws IOException {
assert Files.isSymbolicLink(symlink);
if (Files.notExists(symlink)) { // following link: target does not exist
Files.delete(symlink);
}
}
private void ensureIsEmpty(Path dir) throws IOException {
try (var ds = Files.newDirectoryStream(dir)) {
if (ds.iterator().hasNext()){
throw new DirectoryNotEmptyException(dir.toString());
}
}
}
}
@@ -1,87 +0,0 @@
package org.cryptomator.common.mountpoint;
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.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
class TemporaryMountPointChooser implements MountPointChooser {
private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
private final VaultSettings vaultSettings;
private final Environment environment;
private final MountPointHelper helper;
@Inject
public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, MountPointHelper helper) {
this.vaultSettings = vaultSettings;
this.environment = environment;
this.helper = helper;
}
@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) {
assert environment.getMountPointsDir().isPresent();
//clean leftovers of not-regularly unmounted vaults
//see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061
helper.clearIrregularUnmountDebrisIfNeeded();
return this.environment.getMountPointsDir().map(dir -> this.helper.chooseTemporaryMountPoint(this.vaultSettings, dir));
}
@Override
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
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 "UNUSED_ROOT_DIR, 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());
}
}
}
@@ -2,7 +2,6 @@ package org.cryptomator.common.settings;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Suppliers; import com.google.common.base.Suppliers;
import com.google.common.io.BaseEncoding;
import org.cryptomator.common.Environment; import org.cryptomator.common.Environment;
import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.cryptolib.common.P384KeyPair; import org.cryptomator.cryptolib.common.P384KeyPair;
@@ -16,6 +15,7 @@ import javax.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
@@ -26,6 +26,7 @@ public class DeviceKey {
private static final Logger LOG = LoggerFactory.getLogger(DeviceKey.class); private static final Logger LOG = LoggerFactory.getLogger(DeviceKey.class);
private static final String KEYCHAIN_KEY = "cryptomator-device-p12"; private static final String KEYCHAIN_KEY = "cryptomator-device-p12";
private static final String KEYCHAIN_DISPLAY_NAME = "Cryptomator Device Keypair .p12 Passphrase";
private final KeychainManager keychainManager; private final KeychainManager keychainManager;
private final Environment env; private final Environment env;
@@ -46,15 +47,16 @@ public class DeviceKey {
} }
private P384KeyPair loadOrCreate() throws DeviceKeyRetrievalException { private P384KeyPair loadOrCreate() throws DeviceKeyRetrievalException {
Path p12File = env.getP12Path().findFirst().orElseThrow(() -> new DeviceKeyRetrievalException("No path for .p12 file configured"));
char[] passphrase = null; char[] passphrase = null;
try { try {
passphrase = keychainManager.loadPassphrase(KEYCHAIN_KEY); passphrase = keychainManager.loadPassphrase(KEYCHAIN_KEY);
if (passphrase != null) { if (passphrase != null && Files.isRegularFile(p12File)) {
return loadExistingKeyPair(passphrase); return loadExistingKeyPair(passphrase, p12File);
} else { } else { // (re)generate new key pair if either file or password got lost
passphrase = randomPassword(); passphrase = randomPassword();
keychainManager.storePassphrase(KEYCHAIN_KEY, CharBuffer.wrap(passphrase)); keychainManager.storePassphrase(KEYCHAIN_KEY, KEYCHAIN_DISPLAY_NAME, CharBuffer.wrap(passphrase));
return createAndStoreNewKeyPair(passphrase); return createAndStoreNewKeyPair(passphrase, p12File);
} }
} catch (KeychainAccessException e) { } catch (KeychainAccessException e) {
throw new DeviceKeyRetrievalException("Failed to access system keychain", e); throw new DeviceKeyRetrievalException("Failed to access system keychain", e);
@@ -67,19 +69,12 @@ public class DeviceKey {
} }
} }
private P384KeyPair loadExistingKeyPair(char[] passphrase) throws IOException { private P384KeyPair loadExistingKeyPair(char[] passphrase, Path p12File) throws IOException {
var p12File = env.getP12Path() //
.filter(Files::isRegularFile) //
.findFirst() //
.orElseThrow(() -> new DeviceKeyRetrievalException("Missing .p12 file"));
LOG.debug("Loading existing device key from {}", p12File); LOG.debug("Loading existing device key from {}", p12File);
return P384KeyPair.load(p12File, passphrase); return P384KeyPair.load(p12File, passphrase);
} }
private P384KeyPair createAndStoreNewKeyPair(char[] passphrase) throws IOException { private P384KeyPair createAndStoreNewKeyPair(char[] passphrase, Path p12File) throws IOException {
var p12File = env.getP12Path() //
.findFirst() //
.orElseThrow(() -> new DeviceKeyRetrievalException("No path for .p12 file configured"));
var keyPair = P384KeyPair.generate(); var keyPair = P384KeyPair.generate();
LOG.debug("Store new device key to {}", p12File); LOG.debug("Store new device key to {}", p12File);
keyPair.store(p12File, passphrase); keyPair.store(p12File, passphrase);
@@ -33,11 +33,10 @@ public class Settings {
public static final boolean DEFAULT_CHECK_FOR_UPDATES = false; public static final boolean DEFAULT_CHECK_FOR_UPDATES = false;
public static final boolean DEFAULT_START_HIDDEN = false; public static final boolean DEFAULT_START_HIDDEN = false;
public static final boolean DEFAULT_AUTO_CLOSE_VAULTS = false; public static final boolean DEFAULT_AUTO_CLOSE_VAULTS = false;
public static final boolean DEFAULT_USE_KEYCHAIN = true;
public static final int DEFAULT_PORT = 42427; public static final int DEFAULT_PORT = 42427;
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3; 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 boolean DEFAULT_DEBUG_MODE = false;
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = VolumeImpl.FUSE;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT; public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
@Deprecated // to be changed to "whatever is available" eventually @Deprecated // to be changed to "whatever is available" eventually
public static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : "org.cryptomator.linux.keychain.SecretServiceKeychainAccess"; public static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : "org.cryptomator.linux.keychain.SecretServiceKeychainAccess";
@@ -53,11 +52,10 @@ public class Settings {
private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UPDATES); private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UPDATES);
private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN); private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN);
private final BooleanProperty autoCloseVaults = new SimpleBooleanProperty(DEFAULT_AUTO_CLOSE_VAULTS); private final BooleanProperty autoCloseVaults = new SimpleBooleanProperty(DEFAULT_AUTO_CLOSE_VAULTS);
private final BooleanProperty useKeychain = new SimpleBooleanProperty(DEFAULT_USE_KEYCHAIN);
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT); private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS); private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
private final ObjectProperty<WebDavUrlScheme> preferredGvfsScheme = new SimpleObjectProperty<>(DEFAULT_GVFS_SCHEME);
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE); 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<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
private final ObjectProperty<String> keychainProvider = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_PROVIDER); private final ObjectProperty<String> keychainProvider = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_PROVIDER);
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION); private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
@@ -72,6 +70,9 @@ public class Settings {
private final StringProperty language = new SimpleStringProperty(DEFAULT_LANGUAGE); private final StringProperty language = new SimpleStringProperty(DEFAULT_LANGUAGE);
private final StringProperty mountService = new SimpleStringProperty();
private Consumer<Settings> saveCmd; private Consumer<Settings> saveCmd;
/** /**
@@ -85,11 +86,10 @@ public class Settings {
checkForUpdates.addListener(this::somethingChanged); checkForUpdates.addListener(this::somethingChanged);
startHidden.addListener(this::somethingChanged); startHidden.addListener(this::somethingChanged);
autoCloseVaults.addListener(this::somethingChanged); autoCloseVaults.addListener(this::somethingChanged);
useKeychain.addListener(this::somethingChanged);
port.addListener(this::somethingChanged); port.addListener(this::somethingChanged);
numTrayNotifications.addListener(this::somethingChanged); numTrayNotifications.addListener(this::somethingChanged);
preferredGvfsScheme.addListener(this::somethingChanged);
debugMode.addListener(this::somethingChanged); debugMode.addListener(this::somethingChanged);
preferredVolumeImpl.addListener(this::somethingChanged);
theme.addListener(this::somethingChanged); theme.addListener(this::somethingChanged);
keychainProvider.addListener(this::somethingChanged); keychainProvider.addListener(this::somethingChanged);
userInterfaceOrientation.addListener(this::somethingChanged); userInterfaceOrientation.addListener(this::somethingChanged);
@@ -102,6 +102,7 @@ public class Settings {
windowHeight.addListener(this::somethingChanged); windowHeight.addListener(this::somethingChanged);
displayConfiguration.addListener(this::somethingChanged); displayConfiguration.addListener(this::somethingChanged);
language.addListener(this::somethingChanged); language.addListener(this::somethingChanged);
mountService.addListener(this::somethingChanged);
} }
void setSaveCmd(Consumer<Settings> saveCmd) { void setSaveCmd(Consumer<Settings> saveCmd) {
@@ -140,6 +141,8 @@ public class Settings {
return autoCloseVaults; return autoCloseVaults;
} }
public BooleanProperty useKeychain() { return useKeychain; }
public IntegerProperty port() { public IntegerProperty port() {
return port; return port;
} }
@@ -148,16 +151,12 @@ public class Settings {
return numTrayNotifications; return numTrayNotifications;
} }
public ObjectProperty<WebDavUrlScheme> preferredGvfsScheme() {
return preferredGvfsScheme;
}
public BooleanProperty debugMode() { public BooleanProperty debugMode() {
return debugMode; return debugMode;
} }
public ObjectProperty<VolumeImpl> preferredVolumeImpl() { public StringProperty mountService() {
return preferredVolumeImpl; return mountService;
} }
public ObjectProperty<UiTheme> theme() { public ObjectProperty<UiTheme> theme() {
@@ -205,4 +204,5 @@ public class Settings {
public StringProperty languageProperty() { public StringProperty languageProperty() {
return language; return language;
} }
} }
@@ -44,12 +44,11 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
out.name("autoCloseVaults").value(value.autoCloseVaults().get()); out.name("autoCloseVaults").value(value.autoCloseVaults().get());
out.name("port").value(value.port().get()); out.name("port").value(value.port().get());
out.name("numTrayNotifications").value(value.numTrayNotifications().get()); out.name("numTrayNotifications").value(value.numTrayNotifications().get());
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get().name());
out.name("debugMode").value(value.debugMode().get()); out.name("debugMode").value(value.debugMode().get());
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
out.name("theme").value(value.theme().get().name()); out.name("theme").value(value.theme().get().name());
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name()); out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
out.name("keychainProvider").value(value.keychainProvider().get()); out.name("keychainProvider").value(value.keychainProvider().get());
out.name("useKeychain").value(value.useKeychain().get());
out.name("licenseKey").value(value.licenseKey().get()); out.name("licenseKey").value(value.licenseKey().get());
out.name("showMinimizeButton").value(value.showMinimizeButton().get()); out.name("showMinimizeButton").value(value.showMinimizeButton().get());
out.name("showTrayIcon").value(value.showTrayIcon().get()); out.name("showTrayIcon").value(value.showTrayIcon().get());
@@ -59,7 +58,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
out.name("windowHeight").value((value.windowHeightProperty().get())); out.name("windowHeight").value((value.windowHeightProperty().get()));
out.name("displayConfiguration").value((value.displayConfigurationProperty().get())); out.name("displayConfiguration").value((value.displayConfigurationProperty().get()));
out.name("language").value((value.languageProperty().get())); out.name("language").value((value.languageProperty().get()));
out.name("mountService").value(value.mountService().get());
out.endObject(); out.endObject();
} }
@@ -86,12 +85,11 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "autoCloseVaults" -> settings.autoCloseVaults().set(in.nextBoolean()); case "autoCloseVaults" -> settings.autoCloseVaults().set(in.nextBoolean());
case "port" -> settings.port().set(in.nextInt()); case "port" -> settings.port().set(in.nextInt());
case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt()); case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
case "preferredGvfsScheme" -> settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
case "debugMode" -> settings.debugMode().set(in.nextBoolean()); case "debugMode" -> settings.debugMode().set(in.nextBoolean());
case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
case "theme" -> settings.theme().set(parseUiTheme(in.nextString())); case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString())); case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
case "keychainProvider" -> settings.keychainProvider().set(in.nextString()); case "keychainProvider" -> settings.keychainProvider().set(in.nextString());
case "useKeychain" -> settings.useKeychain().set(in.nextBoolean());
case "licenseKey" -> settings.licenseKey().set(in.nextString()); case "licenseKey" -> settings.licenseKey().set(in.nextString());
case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean()); case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean());
case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean()); case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean());
@@ -101,9 +99,15 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "windowHeight" -> settings.windowHeightProperty().set(in.nextInt()); case "windowHeight" -> settings.windowHeightProperty().set(in.nextInt());
case "displayConfiguration" -> settings.displayConfigurationProperty().set(in.nextString()); case "displayConfiguration" -> settings.displayConfigurationProperty().set(in.nextString());
case "language" -> settings.languageProperty().set(in.nextString()); case "language" -> settings.languageProperty().set(in.nextString());
case "mountService" -> {
var token = in.peek();
if (JsonToken.STRING == token) {
settings.mountService().set(in.nextString());
}
}
default -> { default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name); LOG.warn("Unsupported vault setting found in JSON: {}", name);
in.skipValue(); in.skipValue();
} }
} }
@@ -113,24 +117,6 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
return settings; return settings;
} }
private VolumeImpl parsePreferredVolumeImplName(String nioAdapterName) {
try {
return VolumeImpl.valueOf(nioAdapterName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid volume type {}. Defaulting to {}.", nioAdapterName, Settings.DEFAULT_PREFERRED_VOLUME_IMPL);
return Settings.DEFAULT_PREFERRED_VOLUME_IMPL;
}
}
private WebDavUrlScheme parseWebDavUrlSchemePrefix(String webDavUrlSchemeName) {
try {
return WebDavUrlScheme.valueOf(webDavUrlSchemeName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid WebDAV url scheme {}. Defaulting to {}.", webDavUrlSchemeName, Settings.DEFAULT_GVFS_SCHEME);
return Settings.DEFAULT_GVFS_SCHEME;
}
}
private UiTheme parseUiTheme(String uiThemeName) { private UiTheme parseUiTheme(String uiThemeName) {
try { try {
return UiTheme.valueOf(uiThemeName.toUpperCase()); return UiTheme.valueOf(uiThemeName.toUpperCase());
@@ -6,12 +6,11 @@
package org.cryptomator.common.settings; package org.cryptomator.common.settings;
import com.google.common.base.CharMatcher; import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding; import com.google.common.io.BaseEncoding;
import javafx.beans.Observable; import javafx.beans.Observable;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding; import javafx.beans.binding.StringExpression;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
@@ -22,7 +21,6 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Random; import java.util.Random;
/** /**
@@ -32,7 +30,6 @@ public class VaultSettings {
public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false; public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false;
public static final boolean DEFAULT_REVEAL_AFTER_MOUNT = true; public static final boolean DEFAULT_REVEAL_AFTER_MOUNT = true;
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
public static final boolean DEFAULT_USES_READONLY_MODE = false; public static final boolean DEFAULT_USES_READONLY_MODE = false;
public static final String DEFAULT_MOUNT_FLAGS = ""; public static final String DEFAULT_MOUNT_FLAGS = "";
public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1; public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
@@ -45,26 +42,32 @@ public class VaultSettings {
private final String id; private final String id;
private final ObjectProperty<Path> path = new SimpleObjectProperty<>(); private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
private final StringProperty displayName = new SimpleStringProperty(); private final StringProperty displayName = new SimpleStringProperty();
private final StringProperty winDriveLetter = new SimpleStringProperty();
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP); private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REVEAL_AFTER_MOUNT); private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REVEAL_AFTER_MOUNT);
private final BooleanProperty useCustomMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
private final StringProperty customMountPath = new SimpleStringProperty();
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE); private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS); private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH); private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK); private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE); private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE);
private final IntegerProperty autoLockIdleSeconds = new SimpleIntegerProperty(DEFAULT_AUTOLOCK_IDLE_SECONDS); private final IntegerProperty autoLockIdleSeconds = new SimpleIntegerProperty(DEFAULT_AUTOLOCK_IDLE_SECONDS);
private final StringBinding mountName; private final StringExpression mountName;
private final ObjectProperty<Path> mountPoint = new SimpleObjectProperty<>();
public VaultSettings(String id) { public VaultSettings(String id) {
this.id = Objects.requireNonNull(id); this.id = Objects.requireNonNull(id);
this.mountName = Bindings.createStringBinding(this::normalizeDisplayName, displayName); this.mountName = StringExpression.stringExpression(Bindings.createStringBinding(() -> {
final String name;
if (displayName.isEmpty().get()) {
name = path.get().getFileName().toString();
} else {
name = displayName.get();
}
return normalizeDisplayName(name);
}, displayName, path));
} }
Observable[] observables() { Observable[] observables() {
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock, autoLockWhenIdle, autoLockIdleSeconds}; return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode};
} }
public static VaultSettings withRandomId() { public static VaultSettings withRandomId() {
@@ -78,8 +81,7 @@ public class VaultSettings {
} }
//visible for testing //visible for testing
String normalizeDisplayName() { static String normalizeDisplayName(String original) {
var original = displayName.getValueSafe();
if (original.isBlank() || ".".equals(original) || "..".equals(original)) { if (original.isBlank() || ".".equals(original) || "..".equals(original)) {
return "_"; return "_";
} }
@@ -105,22 +107,10 @@ public class VaultSettings {
return displayName; return displayName;
} }
public StringBinding mountName() { public StringExpression mountName() {
return mountName; return mountName;
} }
public StringProperty winDriveLetter() {
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() { public BooleanProperty unlockAfterStartup() {
return unlockAfterStartup; return unlockAfterStartup;
} }
@@ -129,20 +119,12 @@ public class VaultSettings {
return revealAfterMount; return revealAfterMount;
} }
public BooleanProperty useCustomMountPath() { public Path getMountPoint() {
return useCustomMountPath; return mountPoint.get();
} }
public StringProperty customMountPath() { public ObjectProperty<Path> mountPoint() {
return customMountPath; return mountPoint;
}
public Optional<String> getCustomMountPath() {
if (useCustomMountPath.get()) {
return Optional.ofNullable(Strings.emptyToNull(customMountPath.get()));
} else {
return Optional.empty();
}
} }
public BooleanProperty usesReadOnlyMode() { public BooleanProperty usesReadOnlyMode() {
@@ -188,5 +170,4 @@ public class VaultSettings {
return false; return false;
} }
} }
} }
@@ -6,11 +6,14 @@
package org.cryptomator.common.settings; package org.cryptomator.common.settings;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter; import com.google.gson.stream.JsonWriter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
class VaultSettingsJsonAdapter { class VaultSettingsJsonAdapter {
@@ -22,11 +25,10 @@ class VaultSettingsJsonAdapter {
out.name("id").value(value.getId()); out.name("id").value(value.getId());
out.name("path").value(value.path().get().toString()); out.name("path").value(value.path().get().toString());
out.name("displayName").value(value.displayName().get()); out.name("displayName").value(value.displayName().get());
out.name("winDriveLetter").value(value.winDriveLetter().get());
out.name("unlockAfterStartup").value(value.unlockAfterStartup().get()); out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
out.name("revealAfterMount").value(value.revealAfterMount().get()); out.name("revealAfterMount").value(value.revealAfterMount().get());
out.name("useCustomMountPath").value(value.useCustomMountPath().get()); var mountPoint = value.mountPoint().get();
out.name("customMountPath").value(value.customMountPath().get()); out.name("mountPoint").value(mountPoint != null ? mountPoint.toAbsolutePath().toString() : null);
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get()); out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
out.name("mountFlags").value(value.mountFlags().get()); out.name("mountFlags").value(value.mountFlags().get());
out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get()); out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get());
@@ -36,18 +38,18 @@ class VaultSettingsJsonAdapter {
out.endObject(); out.endObject();
} }
//TODO: usesCustomMountPath, customMountPath and winDriveLetter removed
// -> migration required
public VaultSettings read(JsonReader in) throws IOException { public VaultSettings read(JsonReader in) throws IOException {
String id = null; String id = null;
String path = null; String path = null;
String mountName = null; //see https://github.com/cryptomator/cryptomator/pull/1318 String mountName = null; //see https://github.com/cryptomator/cryptomator/pull/1318
String displayName = null; String displayName = null;
String customMountPath = null;
String winDriveLetter = null;
boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP; boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
boolean revealAfterMount = VaultSettings.DEFAULT_REVEAL_AFTER_MOUNT; boolean revealAfterMount = VaultSettings.DEFAULT_REVEAL_AFTER_MOUNT;
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE; boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS; String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
Path mountPoint = null;
int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH; int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK; WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
boolean autoLockWhenIdle = VaultSettings.DEFAULT_AUTOLOCK_WHEN_IDLE; boolean autoLockWhenIdle = VaultSettings.DEFAULT_AUTOLOCK_WHEN_IDLE;
@@ -61,19 +63,23 @@ class VaultSettingsJsonAdapter {
case "path" -> path = in.nextString(); case "path" -> path = in.nextString();
case "mountName" -> mountName = in.nextString(); //see https://github.com/cryptomator/cryptomator/pull/1318 case "mountName" -> mountName = in.nextString(); //see https://github.com/cryptomator/cryptomator/pull/1318
case "displayName" -> displayName = in.nextString(); case "displayName" -> displayName = in.nextString();
case "winDriveLetter" -> winDriveLetter = in.nextString();
case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean(); case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean();
case "revealAfterMount" -> revealAfterMount = in.nextBoolean(); case "revealAfterMount" -> revealAfterMount = in.nextBoolean();
case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean(); case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
case "mountFlags" -> mountFlags = in.nextString(); case "mountFlags" -> mountFlags = in.nextString();
case "mountPoint" -> {
if (JsonToken.NULL == in.peek()) {
in.nextNull();
} else {
mountPoint = parseMountPoint(in.nextString());
}
}
case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt(); case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString()); case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
case "autoLockWhenIdle" -> autoLockWhenIdle = in.nextBoolean(); case "autoLockWhenIdle" -> autoLockWhenIdle = in.nextBoolean();
case "autoLockIdleSeconds" -> autoLockIdleSeconds = in.nextInt(); case "autoLockIdleSeconds" -> autoLockIdleSeconds = in.nextInt();
default -> { default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name); LOG.warn("Unsupported vault setting found in JSON: {}", name);
in.skipValue(); in.skipValue();
} }
} }
@@ -87,20 +93,27 @@ class VaultSettingsJsonAdapter {
vaultSettings.displayName().set(mountName); vaultSettings.displayName().set(mountName);
} }
vaultSettings.path().set(Paths.get(path)); vaultSettings.path().set(Paths.get(path));
vaultSettings.winDriveLetter().set(winDriveLetter);
vaultSettings.unlockAfterStartup().set(unlockAfterStartup); vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
vaultSettings.revealAfterMount().set(revealAfterMount); vaultSettings.revealAfterMount().set(revealAfterMount);
vaultSettings.useCustomMountPath().set(useCustomMountPath);
vaultSettings.customMountPath().set(customMountPath);
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode); vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
vaultSettings.mountFlags().set(mountFlags); vaultSettings.mountFlags().set(mountFlags);
vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength); vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
vaultSettings.actionAfterUnlock().set(actionAfterUnlock); vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
vaultSettings.autoLockWhenIdle().set(autoLockWhenIdle); vaultSettings.autoLockWhenIdle().set(autoLockWhenIdle);
vaultSettings.autoLockIdleSeconds().set(autoLockIdleSeconds); vaultSettings.autoLockIdleSeconds().set(autoLockIdleSeconds);
vaultSettings.mountPoint().set(mountPoint);
return vaultSettings; return vaultSettings;
} }
private Path parseMountPoint(String mountPoint) {
try {
return Path.of(mountPoint);
} catch (InvalidPathException e) {
LOG.warn("Invalid string as mount point. Defaulting to null.");
return null;
}
}
private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) { private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) {
try { try {
return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase()); return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase());
@@ -1,22 +0,0 @@
package org.cryptomator.common.settings;
public enum WebDavUrlScheme {
DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");
private final String prefix;
private final String displayName;
WebDavUrlScheme(String prefix, String displayName) {
this.prefix = prefix;
this.displayName = displayName;
}
public String getPrefix() {
return prefix;
}
public String getDisplayName() {
return displayName;
}
}
@@ -1,46 +0,0 @@
package org.cryptomator.common.vaults;
import com.google.common.collect.Iterables;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.mountpoint.MountPointChooser;
import java.nio.file.Path;
import java.util.Optional;
public abstract class AbstractVolume implements Volume {
private final Iterable<MountPointChooser> choosers;
protected Path mountPoint;
private boolean cleanupRequired;
private MountPointChooser usedChooser;
public AbstractVolume(Iterable<MountPointChooser> choosers) {
this.choosers = choosers;
}
protected Path determineMountPoint() throws InvalidMountPointException {
var applicableChoosers = Iterables.filter(choosers, c -> c.isApplicable(this));
for (var chooser : applicableChoosers) {
Optional<Path> chosenPath = chooser.chooseMountPoint(this);
if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint
continue;
}
this.cleanupRequired = chooser.prepare(this, chosenPath.get());
this.usedChooser = chooser;
return chosenPath.get();
}
throw new InvalidMountPointException(String.format("No feasible MountPoint found by choosers: %s", applicableChoosers));
}
protected void cleanupMountPoint() {
if (this.cleanupRequired) {
this.usedChooser.cleanup(this, this.mountPoint);
}
}
@Override
public Optional<Path> getMountPoint() {
return Optional.ofNullable(mountPoint);
}
}
@@ -1,11 +1,13 @@
package org.cryptomator.common.vaults; package org.cryptomator.common.vaults;
import org.cryptomator.integrations.mount.UnmountFailedException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -39,14 +41,13 @@ public class AutoLocker {
try { try {
vault.lock(false); vault.lock(false);
LOG.info("Autolocked {} after idle timeout", vault.getDisplayName()); LOG.info("Autolocked {} after idle timeout", vault.getDisplayName());
} catch (Volume.VolumeException | LockNotCompletedException e) { } catch (UnmountFailedException | IOException e) {
LOG.error("Autolocking failed.", e); LOG.error("Autolocking failed.", e);
} }
} }
private boolean exceedsIdleTime(Vault vault) { private boolean exceedsIdleTime(Vault vault) {
assert vault.isUnlocked(); assert vault.isUnlocked();
// TODO: shouldn't we read these properties from within FX Application Thread?
if (vault.getVaultSettings().autoLockWhenIdle().get()) { if (vault.getVaultSettings().autoLockWhenIdle().get()) {
int maxIdleSeconds = vault.getVaultSettings().autoLockIdleSeconds().get(); int maxIdleSeconds = vault.getVaultSettings().autoLockIdleSeconds().get();
var deadline = vault.getStats().getLastActivity().plusSeconds(maxIdleSeconds); var deadline = vault.getStats().getLastActivity().plusSeconds(maxIdleSeconds);
@@ -1,14 +0,0 @@
package org.cryptomator.common.vaults;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@interface DefaultMountFlags {
}
@@ -1,95 +0,0 @@
package org.cryptomator.common.vaults;
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.DokanyMountFailedException;
import org.cryptomator.frontend.dokany.Mount;
import org.cryptomator.frontend.dokany.MountFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.function.Consumer;
public class DokanyVolume extends AbstractVolume {
private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class);
private static final String FS_TYPE_NAME = "CryptomatorFS";
private final VaultSettings vaultSettings;
private Mount mount;
@Inject
public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
super(choosers);
this.vaultSettings = vaultSettings;
}
@Override
public VolumeImpl getImplementationType() {
return VolumeImpl.DOKANY;
}
@Override
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
this.mountPoint = determineMountPoint();
try {
this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction);
} catch (DokanyMountFailedException e) {
if (vaultSettings.getCustomMountPath().isPresent()) {
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
}
throw new VolumeException("Unable to mount Filesystem", e);
}
}
@Override
public void reveal(Revealer revealer) throws VolumeException {
try {
mount.reveal(revealer::reveal);
} catch (Exception e) {
throw new VolumeException(e);
}
}
@Override
public void unmount() throws VolumeException {
try {
mount.unmount();
} catch (IllegalStateException e) {
throw new VolumeException("Unmount Failed.", e);
}
cleanupMountPoint();
}
@Override
public void unmountForced() {
mount.unmountForced();
cleanupMountPoint();
}
@Override
public boolean supportsForcedUnmount() {
return true;
}
@Override
public boolean isSupported() {
return DokanyVolume.isSupportedStatic();
}
@Override
public MountPointRequirement getMountPointRequirement() {
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.EMPTY_MOUNT_POINT;
}
public static boolean isSupportedStatic() {
return MountFactory.isApplicable();
}
}
@@ -1,136 +0,0 @@
package org.cryptomator.common.vaults;
import com.google.common.collect.Iterators;
import org.apache.commons.lang3.SystemUtils;
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.fuse.mount.EnvironmentVariables;
import org.cryptomator.frontend.fuse.mount.FuseMountException;
import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
import org.cryptomator.frontend.fuse.mount.Mount;
import org.cryptomator.frontend.fuse.mount.Mounter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;
public class FuseVolume extends AbstractVolume {
private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class);
private static final Pattern NON_WHITESPACE_OR_QUOTED = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); // Thanks to https://stackoverflow.com/a/366532
private final VaultSettings vaultSettings;
private Mount mount;
@Inject
public FuseVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
super(choosers);
this.vaultSettings = vaultSettings;
}
@Override
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
this.mountPoint = determineMountPoint();
mount(fs.getPath("/"), mountFlags, onExitAction);
}
private void mount(Path root, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
try {
Mounter mounter = FuseMountFactory.getMounter();
EnvironmentVariables envVars = EnvironmentVariables.create() //
.withFlags(splitFlags(mountFlags)) //
.withMountPoint(mountPoint) //
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
.build();
this.mount = mounter.mount(root, envVars, onExitAction);
} catch (FuseMountException | FuseNotSupportedException e) {
throw new VolumeException("Unable to mount Filesystem", e);
}
}
private String[] splitFlags(String str) {
List<String> flags = new ArrayList<>();
var matches = Iterators.peekingIterator(NON_WHITESPACE_OR_QUOTED.matcher(str).results().iterator());
while (matches.hasNext()) {
String flag = matches.next().group();
// check if flag is missing its argument:
if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(1) != null) { // next is "double quoted"
// next is "double quoted" and flag is missing its argument
flag += matches.next().group(1);
} else if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(2) != null) {
// next is 'single quoted' and flag is missing its argument
flag += matches.next().group(2);
}
flags.add(flag);
}
return flags.toArray(String[]::new);
}
@Override
public void reveal(Revealer revealer) throws VolumeException {
try {
mount.reveal(revealer::reveal);
} catch (Exception e) {
throw new VolumeException(e);
}
}
@Override
public boolean supportsForcedUnmount() {
return true;
}
@Override
public synchronized void unmountForced() throws VolumeException {
try {
mount.unmountForced();
} catch (FuseMountException e) {
throw new VolumeException(e);
}
cleanupMountPoint();
}
@Override
public synchronized void unmount() throws VolumeException {
try {
mount.unmount();
} catch (FuseMountException e) {
throw new VolumeException(e);
}
cleanupMountPoint();
}
@Override
public boolean isSupported() {
return FuseVolume.isSupportedStatic();
}
@Override
public VolumeImpl getImplementationType() {
return VolumeImpl.FUSE;
}
@Override
public MountPointRequirement getMountPointRequirement() {
if (!SystemUtils.IS_OS_WINDOWS) {
return MountPointRequirement.EMPTY_MOUNT_POINT;
}
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.PARENT_NO_MOUNT_POINT;
}
public static boolean isSupportedStatic() {
return FuseMountFactory.isFuseSupported();
}
}
@@ -1,12 +0,0 @@
package org.cryptomator.common.vaults;
public class LockNotCompletedException extends Exception {
public LockNotCompletedException(String reason) {
super(reason);
}
public LockNotCompletedException(Throwable cause) {
super(cause);
}
}
@@ -1,33 +0,0 @@
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 {
/**
* The Mountpoint needs to be a filesystem root and must not exist.
*/
UNUSED_ROOT_DIR,
/**
* 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;
}
@@ -11,9 +11,10 @@ package org.cryptomator.common.vaults;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Constants; import org.cryptomator.common.Constants;
import org.cryptomator.common.mountpoint.InvalidMountPointException; import org.cryptomator.common.Environment;
import org.cryptomator.common.mount.WindowsDriveLetters;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Volume.VolumeException;
import org.cryptomator.cryptofs.CryptoFileSystem; import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags; import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
@@ -22,29 +23,42 @@ import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.integrations.mount.Mount;
import org.cryptomator.integrations.mount.MountBuilder;
import org.cryptomator.integrations.mount.MountCapability;
import org.cryptomator.integrations.mount.MountFailedException;
import org.cryptomator.integrations.mount.MountService;
import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.integrations.mount.UnmountFailedException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider;
import javafx.beans.Observable; import javafx.beans.Observable;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding; import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER;
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR;
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_SYSTEM_CHOSEN_PATH;
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_WITHIN_EXISTING_PARENT;
@PerVault @PerVault
public class Vault { public class Vault {
@@ -52,15 +66,16 @@ public class Vault {
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME); private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE; private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
private final Environment env;
private final Settings settings;
private final VaultSettings vaultSettings; private final VaultSettings vaultSettings;
private final Provider<Volume> volumeProvider;
private final StringBinding defaultMountFlags;
private final AtomicReference<CryptoFileSystem> cryptoFileSystem; private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
private final VaultState state; private final VaultState state;
private final ObjectProperty<Exception> lastKnownException; private final ObjectProperty<Exception> lastKnownException;
private final ObservableValue<MountService> mountService;
private final ObservableValue<String> defaultMountFlags;
private final VaultConfigCache configCache; private final VaultConfigCache configCache;
private final VaultStats stats; private final VaultStats stats;
private final StringBinding displayName;
private final StringBinding displayablePath; private final StringBinding displayablePath;
private final BooleanBinding locked; private final BooleanBinding locked;
private final BooleanBinding processing; private final BooleanBinding processing;
@@ -68,23 +83,24 @@ public class Vault {
private final BooleanBinding missing; private final BooleanBinding missing;
private final BooleanBinding needsMigration; private final BooleanBinding needsMigration;
private final BooleanBinding unknownError; private final BooleanBinding unknownError;
private final StringBinding accessPoint; private final ObjectBinding<Mountpoint> mountPoint;
private final BooleanBinding accessPointPresent; private final WindowsDriveLetters windowsDriveLetters;
private final BooleanProperty showingStats; private final BooleanProperty showingStats;
private volatile Volume volume; private AtomicReference<MountHandle> mountHandle = new AtomicReference<>(null);
@Inject @Inject
Vault(VaultSettings vaultSettings, VaultConfigCache configCache, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) { Vault(Environment env, Settings settings, VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, ObservableValue<MountService> mountService, VaultStats stats, WindowsDriveLetters windowsDriveLetters) {
this.env = env;
this.settings = settings;
this.vaultSettings = vaultSettings; this.vaultSettings = vaultSettings;
this.configCache = configCache; this.configCache = configCache;
this.volumeProvider = volumeProvider;
this.defaultMountFlags = defaultMountFlags;
this.cryptoFileSystem = cryptoFileSystem; this.cryptoFileSystem = cryptoFileSystem;
this.state = state; this.state = state;
this.lastKnownException = lastKnownException; this.lastKnownException = lastKnownException;
this.mountService = mountService;
this.defaultMountFlags = mountService.map(MountService::getDefaultMountFlags);
this.stats = stats; this.stats = stats;
this.displayName = Bindings.createStringBinding(this::getDisplayName, vaultSettings.displayName());
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path()); this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
this.locked = Bindings.createBooleanBinding(this::isLocked, state); this.locked = Bindings.createBooleanBinding(this::isLocked, state);
this.processing = Bindings.createBooleanBinding(this::isProcessing, state); this.processing = Bindings.createBooleanBinding(this::isProcessing, state);
@@ -92,8 +108,8 @@ public class Vault {
this.missing = Bindings.createBooleanBinding(this::isMissing, state); this.missing = Bindings.createBooleanBinding(this::isMissing, state);
this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state); this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state); this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
this.accessPoint = Bindings.createStringBinding(this::getAccessPoint, state); this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state);
this.accessPointPresent = this.accessPoint.isNotEmpty(); this.windowsDriveLetters = windowsDriveLetters;
this.showingStats = new SimpleBooleanProperty(false); this.showingStats = new SimpleBooleanProperty(false);
} }
@@ -143,7 +159,46 @@ public class Vault {
} }
} }
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException { private MountBuilder prepareMount(Path cryptoRoot) throws IOException {
var mountProvider = mountService.getValue();
var builder = mountProvider.forFileSystem(cryptoRoot);
for (var capability : mountProvider.capabilities()) {
switch (capability) {
case FILE_SYSTEM_NAME -> builder.setFileSystemName("crypto");
case LOOPBACK_PORT -> builder.setLoopbackPort(settings.port().get()); //TODO: move port from settings to vaultsettings?
case LOOPBACK_HOST_NAME -> builder.setLoopbackHostName("cryptomator-vault"); //TODO: Read from system property
case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode().get());
case MOUNT_FLAGS -> builder.setMountFlags(defaultMountFlags.getValue()); // TODO use custom mount flags (pre-populated with default mount flags)
case VOLUME_ID -> builder.setVolumeId(vaultSettings.getId());
case VOLUME_NAME -> builder.setVolumeName(vaultSettings.mountName().get());
}
}
var userChosenMountPoint = vaultSettings.getMountPoint();
var defaultMountPointBase = env.getMountPointsDir().orElseThrow();
if (userChosenMountPoint == null) {
if (mountProvider.hasCapability(MOUNT_TO_SYSTEM_CHOSEN_PATH)) {
// no need to set a mount point
} else if (mountProvider.hasCapability(MOUNT_AS_DRIVE_LETTER)) {
builder.setMountpoint(windowsDriveLetters.getFirstDesiredAvailable().orElseThrow());
} else if (mountProvider.hasCapability(MOUNT_WITHIN_EXISTING_PARENT)) {
Files.createDirectories(defaultMountPointBase);
builder.setMountpoint(defaultMountPointBase);
} else if (mountProvider.hasCapability(MOUNT_TO_EXISTING_DIR) ) {
var mountPoint = defaultMountPointBase.resolve(vaultSettings.mountName().get());
Files.createDirectories(mountPoint);
builder.setMountpoint(mountPoint);
}
} else if (mountProvider.hasCapability(MOUNT_TO_EXISTING_DIR) || mountProvider.hasCapability(MOUNT_WITHIN_EXISTING_PARENT) || mountProvider.hasCapability(MOUNT_AS_DRIVE_LETTER)) {
// TODO: move the mount point away in case of MOUNT_WITHIN_EXISTING_PARENT?
builder.setMountpoint(userChosenMountPoint);
}
return builder;
}
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, MountFailedException {
if (cryptoFileSystem.get() != null) { if (cryptoFileSystem.get() != null) {
throw new IllegalStateException("Already unlocked."); throw new IllegalStateException("Already unlocked.");
} }
@@ -151,9 +206,10 @@ public class Vault {
boolean success = false; boolean success = false;
try { try {
cryptoFileSystem.set(fs); cryptoFileSystem.set(fs);
volume = volumeProvider.get(); var rootPath = fs.getRootDirectories().iterator().next();
volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit); var supportsForcedUnmount = mountService.getValue().hasCapability(MountCapability.UNMOUNT_FORCED);
success = true; var mountHandle = new MountHandle(prepareMount(rootPath).mount(), supportsForcedUnmount);
success = this.mountHandle.compareAndSet(null, mountHandle);
} finally { } finally {
if (!success) { if (!success) {
destroyCryptoFileSystem(); destroyCryptoFileSystem();
@@ -161,37 +217,28 @@ public class Vault {
} }
} }
private void lockOnVolumeExit(Throwable t) {
LOG.info("Unmounted vault '{}'", getDisplayName());
destroyCryptoFileSystem();
state.set(VaultState.Value.LOCKED);
if (t != null) {
LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
}
}
public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException { public synchronized void lock(boolean forced) throws UnmountFailedException, IOException {
//initiate unmount var mountHandle = this.mountHandle.get();
if (forced && volume.supportsForcedUnmount()) { if (mountHandle == null) {
volume.unmountForced(); //TODO: noop or InvalidStateException?
return;
}
if (forced && mountHandle.supportsUnmountForced) {
mountHandle.mount.unmountForced();
} else { } else {
volume.unmount(); mountHandle.mount.unmount();
} }
//wait for lockOnVolumeExit to be executed
try { try {
boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS); mountHandle.mount.close();
if (!locked) { } finally {
throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress."); destroyCryptoFileSystem();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LockNotCompletedException(e);
} }
}
public void reveal(Volume.Revealer vaultRevealer) throws VolumeException { this.mountHandle.set(null);
volume.reveal(vaultRevealer); LOG.info("Locked vault '{}'", getDisplayName());
} }
// ****************************************************************************** // ******************************************************************************
@@ -266,33 +313,21 @@ public class Vault {
return state.get() == VaultState.Value.ERROR; return state.get() == VaultState.Value.ERROR;
} }
public StringBinding displayNameProperty() { public ReadOnlyStringProperty displayNameProperty() {
return displayName; return vaultSettings.displayName();
} }
public String getDisplayName() { public String getDisplayName() {
return vaultSettings.displayName().get(); return vaultSettings.displayName().get();
} }
public StringBinding accessPointProperty() { public ObjectBinding<Mountpoint> mountPointProperty() {
return accessPoint; return mountPoint;
} }
public String getAccessPoint() { public Mountpoint getMountPoint() {
if (state.getValue() == VaultState.Value.UNLOCKED) { var handle = mountHandle.get();
assert volume != null; return handle == null ? null : handle.mount.getMountpoint();
return volume.getMountPoint().orElse(Path.of("")).toString();
} else {
return "";
}
}
public BooleanBinding accessPointPresentProperty() {
return accessPointPresent;
}
public boolean isAccessPointPresent() {
return accessPointPresent.get();
} }
public StringBinding displayablePathProperty() { public StringBinding displayablePathProperty() {
@@ -315,7 +350,7 @@ public class Vault {
} }
public boolean isShowingStats() { public boolean isShowingStats() {
return accessPointPresent.get(); return mountHandle.get() != null;
} }
@@ -344,18 +379,18 @@ public class Vault {
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get()); return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
} }
public StringBinding defaultMountFlagsProperty() { public ObservableValue<String> defaultMountFlagsProperty() {
return defaultMountFlags; return defaultMountFlags;
} }
public String getDefaultMountFlags() { public String getDefaultMountFlags() {
return defaultMountFlags.get(); return defaultMountFlags.getValue();
} }
public String getEffectiveMountFlags() { public String getEffectiveMountFlags() {
String mountFlags = vaultSettings.mountFlags().get(); String mountFlags = vaultSettings.mountFlags().get();
if (Strings.isNullOrEmpty(mountFlags)) { if (Strings.isNullOrEmpty(mountFlags)) {
return getDefaultMountFlags(); return ""; //TODO: should the provider provide dem defaults??
} else { } else {
return mountFlags; return mountFlags;
} }
@@ -373,10 +408,6 @@ public class Vault {
return vaultSettings.getId(); return vaultSettings.getId();
} }
public Optional<Volume> getVolume() {
return Optional.ofNullable(this.volume);
}
// ****************************************************************************** // ******************************************************************************
// Hashcode / Equals // Hashcode / Equals
// *******************************************************************************/ // *******************************************************************************/
@@ -395,7 +426,15 @@ public class Vault {
} }
} }
/* TODO: reactivate/ needed at all?
public boolean supportsForcedUnmount() { public boolean supportsForcedUnmount() {
return volume.supportsForcedUnmount(); return volume.supportsForcedUnmount();
} }
*/
private record MountHandle(Mount mount, boolean supportsUnmountForced) {
}
} }
@@ -8,13 +8,12 @@ package org.cryptomator.common.vaults;
import dagger.BindsInstance; import dagger.BindsInstance;
import dagger.Subcomponent; import dagger.Subcomponent;
import org.cryptomator.common.Nullable; import org.cryptomator.common.Nullable;
import org.cryptomator.common.mountpoint.MountPointChooserModule;
import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.settings.VaultSettings;
import javax.inject.Named; import javax.inject.Named;
@PerVault @PerVault
@Subcomponent(modules = {VaultModule.class, MountPointChooserModule.class}) @Subcomponent(modules = {VaultModule.class})
public interface VaultComponent { public interface VaultComponent {
Vault vault(); Vault vault();
@@ -7,26 +7,14 @@ package org.cryptomator.common.vaults;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Nullable; import org.cryptomator.common.Nullable;
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.cryptofs.CryptoFileSystem;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Named; 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.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@Module @Module
@@ -47,130 +35,4 @@ public class VaultModule {
return new SimpleObjectProperty<>(initialErrorCause); return new SimpleObjectProperty<>(initialErrorCause);
} }
@Provides
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
if (VolumeImpl.DOKANY == preferredImpl && dokanyVolume.isSupported()) {
return dokanyVolume;
} else if (VolumeImpl.FUSE == preferredImpl && fuseVolume.isSupported()) {
return fuseVolume;
} else {
if (VolumeImpl.WEBDAV != preferredImpl) {
LOG.warn("Using WebDAV, because {} is not supported.", preferredImpl.getDisplayName());
}
assert webDavVolume.isSupported();
return webDavVolume;
}
}
@Provides
@PerVault
@DefaultMountFlags
public StringBinding provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
ObjectProperty<VolumeImpl> preferredVolumeImpl = settings.preferredVolumeImpl();
StringBinding mountName = vaultSettings.mountName();
BooleanProperty readOnly = vaultSettings.usesReadOnlyMode();
return Bindings.createStringBinding(() -> {
VolumeImpl v = preferredVolumeImpl.get();
if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_MAC) {
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 {
return "--flags-supported-on-FUSE-or-DOKANY-only";
}
}, mountName, readOnly, preferredVolumeImpl);
}
// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
private String getMacFuseDefaultMountFlags(StringBinding mountName, ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_MAC_OSX;
StringBuilder flags = new StringBuilder();
if (readOnly.get()) {
flags.append(" -ordonly");
}
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
flags.append(" -oatomic_o_trunc");
flags.append(" -oauto_xattr");
flags.append(" -oauto_cache");
flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
try {
Path userHome = Paths.get(System.getProperty("user.home"));
int uid = (int) Files.getAttribute(userHome, "unix:uid");
int gid = (int) Files.getAttribute(userHome, "unix:gid");
flags.append(" -ouid=").append(uid);
flags.append(" -ogid=").append(gid);
} catch (IOException e) {
LOG.error("Could not read uid/gid from USER_HOME", e);
}
return flags.toString().strip();
}
// see https://manpages.debian.org/testing/fuse/mount.fuse.8.en.html
private String getLinuxFuseDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_LINUX;
StringBuilder flags = new StringBuilder();
if (readOnly.get()) {
flags.append(" -oro");
}
flags.append(" -oauto_unmount");
try {
Path userHome = Paths.get(System.getProperty("user.home"));
int uid = (int) Files.getAttribute(userHome, "unix:uid");
int gid = (int) Files.getAttribute(userHome, "unix:gid");
flags.append(" -ouid=").append(uid);
flags.append(" -ogid=").append(gid);
} catch (IOException e) {
LOG.error("Could not read uid/gid from USER_HOME", e);
}
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 chosen
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=11");
}
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
//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;
StringBuilder flags = new StringBuilder();
flags.append(" --options CURRENT_SESSION");
if (readOnly.get()) {
flags.append(",WRITE_PROTECTION");
}
flags.append(" --thread-count 5");
flags.append(" --timeout 10000");
flags.append(" --allocation-unit-size 4096");
flags.append(" --sector-size 4096");
return flags.toString().strip();
}
} }
@@ -1,102 +0,0 @@
package org.cryptomator.common.vaults;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.stream.Stream;
/**
* Takes a Volume and uses it to mount an unlocked vault
*/
public interface Volume {
/**
* Checks in constant time whether this volume type is supported on the system running Cryptomator.
*
* @return true if this volume can be mounted
*/
boolean isSupported();
/**
* Gets the corresponding 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, Consumer<Throwable> onExitAction) throws IOException, VolumeException, InvalidMountPointException;
/**
* Reveals the mounted volume.
* <p>
* The given {@code revealer} might be used to do it, but not necessarily.
*
* @param revealer An object capable of revealing the location of the mounted vault to view the content (e.g. in the default file browser).
* @throws VolumeException
*/
void reveal(Revealer revealer) throws VolumeException;
void unmount() throws VolumeException;
Optional<Path> getMountPoint();
MountPointRequirement getMountPointRequirement();
// optional forced unmounting:
default boolean supportsForcedUnmount() {
return false;
}
default void unmountForced() throws VolumeException {
throw new VolumeException("Operation not supported.");
}
static VolumeImpl[] getCurrentSupportedAdapters() {
return Stream.of(VolumeImpl.values()).filter(impl -> switch (impl) {
case WEBDAV -> WebDavVolume.isSupportedStatic();
case DOKANY -> DokanyVolume.isSupportedStatic();
case FUSE -> FuseVolume.isSupportedStatic();
}).toArray(VolumeImpl[]::new);
}
/**
* Exception thrown when a volume-specific command such as mount/unmount/reveal failed.
*/
class VolumeException extends Exception {
public VolumeException(String message) {
super(message);
}
public VolumeException(Throwable cause) {
super(cause);
}
public VolumeException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Hides and unifies the different Revealer implementations in the different nio-adapters.
*/
@FunctionalInterface
interface Revealer {
void reveal(Path p) throws VolumeException;
}
}
@@ -1,170 +0,0 @@
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;
import org.cryptomator.frontend.webdav.mount.Mounter;
import org.cryptomator.frontend.webdav.servlet.WebDavServletController;
import javax.inject.Inject;
import javax.inject.Provider;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class WebDavVolume implements Volume {
private static final String LOCALHOST_ALIAS = "cryptomator-vault";
private final Provider<WebDavServer> serverProvider;
private final VaultSettings vaultSettings;
private final Settings settings;
private final WindowsDriveLetters windowsDriveLetters;
private WebDavServer server;
private WebDavServletController servlet;
private Mounter.Mount mount;
private Consumer<Throwable> onExitAction;
@Inject
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
this.serverProvider = serverProvider;
this.vaultSettings = vaultSettings;
this.settings = settings;
this.windowsDriveLetters = windowsDriveLetters;
}
@Override
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
startServlet(fs);
mountServlet();
this.onExitAction = onExitAction;
}
private void startServlet(CryptoFileSystem fs) {
if (server == null) {
server = serverProvider.get();
}
if (!server.isRunning()) {
server.start();
}
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();
}
private void mountServlet() throws VolumeException {
if (servlet == null) {
throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
}
//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specific one or there is no free.
Supplier<String> driveLetterSupplier;
if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
driveLetterSupplier = () -> windowsDriveLetters.getDesiredAvailableDriveLetter().orElse(null);
} else {
driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
}
MountParams mountParams = MountParams.create() //
.withWindowsDriveLetter(driveLetterSupplier.get()) //
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
.withWebdavHostname(getLocalhostAliasOrNull()) //
.build();
try {
this.mount = servlet.mount(mountParams); // might block this thread for a while
} catch (Mounter.CommandFailedException e) {
throw new VolumeException(e);
}
}
@Override
public void reveal(Revealer revealer) throws VolumeException {
try {
mount.reveal(revealer::reveal);
} catch (Exception e) {
throw new VolumeException(e);
}
}
@Override
public synchronized void unmount() throws VolumeException {
try {
mount.unmount();
} catch (Mounter.CommandFailedException e) {
throw new VolumeException(e);
}
cleanup();
onExitAction.accept(null);
}
@Override
public synchronized void unmountForced() throws VolumeException {
try {
mount.forced().orElseThrow(IllegalStateException::new).unmount();
} catch (Mounter.CommandFailedException e) {
throw new VolumeException(e);
}
cleanup();
onExitAction.accept(null);
}
@Override
public Optional<Path> getMountPoint() {
return mount.getMountPoint();
}
@Override
public MountPointRequirement getMountPointRequirement() {
return MountPointRequirement.NONE;
}
private String getLocalhostAliasOrNull() {
try {
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);
if (alias.getHostAddress().equals("127.0.0.1")) {
return LOCALHOST_ALIAS;
} else {
return null;
}
} catch (UnknownHostException e) {
return null;
}
}
private void cleanup() {
if (servlet != null) {
servlet.stop();
}
}
@Override
public boolean isSupported() {
return WebDavVolume.isSupportedStatic();
}
@Override
public VolumeImpl getImplementationType() {
return VolumeImpl.WEBDAV;
}
@Override
public boolean supportsForcedUnmount() {
return mount != null && mount.forced().isPresent();
}
public static boolean isSupportedStatic() {
return true;
}
}
@@ -1,85 +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.common.vaults;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.SystemUtils;
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;
import java.util.stream.StreamSupport;
@Singleton
public final class WindowsDriveLetters {
private static final Set<String> A_TO_Z;
static {
try (IntStream stream = IntStream.rangeClosed('A', 'Z')) {
A_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(ImmutableSet.toImmutableSet());
}
}
@Inject
public WindowsDriveLetters() {
}
public Set<String> getAllDriveLetters() {
return A_TO_Z;
}
public Set<String> getOccupiedDriveLetters() {
if (!SystemUtils.IS_OS_WINDOWS) {
return Set.of();
} else {
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
return StreamSupport.stream(rootDirs.spliterator(), false).map(p -> p.toString().substring(0, 1)).collect(Collectors.toSet());
}
}
public Set<String> getAvailableDriveLetters() {
return Sets.difference(getAllDriveLetters(), getOccupiedDriveLetters());
}
public Optional<String> getAvailableDriveLetter() {
return getAvailableDriveLetters().stream().findFirst();
}
public Optional<Path> getAvailableDriveLetterPath() {
return getAvailableDriveLetter().map(this::toPath);
}
/**
* Skips A and B and only returns them if all other are occupied.
*
* @return an Optional containing either the letter of a free drive letter or empty, if none is available
*/
public Optional<String> getDesiredAvailableDriveLetter() {
var availableDriveLetters = getAvailableDriveLetters();
var optString = availableDriveLetters.stream().filter(s -> !(s.equals("A") || s.equals("B"))).findFirst();
return optString.or(() -> availableDriveLetters.stream().findFirst());
}
/**
* Skips A and B and only returns them if all other are occupied.
*
* @return an Optional containing either the path to a free drive letter or empty, if none is available
*/
public Optional<Path> getDesiredAvailableDriveLetterPath() {
return getDesiredAvailableDriveLetter().map(this::toPath);
}
public Path toPath(String driveLetter) {
return Path.of(driveLetter + ":\\");
}
}
@@ -12,7 +12,6 @@ import org.cryptomator.common.Environment;
import org.cryptomator.common.ShutdownHook; import org.cryptomator.common.ShutdownHook;
import org.cryptomator.ipc.IpcCommunicator; import org.cryptomator.ipc.IpcCommunicator;
import org.cryptomator.logging.DebugMode; import org.cryptomator.logging.DebugMode;
import org.cryptomator.logging.LoggerConfiguration;
import org.cryptomator.ui.fxapp.FxApplicationComponent; import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -35,7 +34,6 @@ public class Cryptomator {
private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.factory().create(STARTUP_TIME); private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.factory().create(STARTUP_TIME);
private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class); private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
private final LoggerConfiguration logConfig;
private final DebugMode debugMode; private final DebugMode debugMode;
private final SupportedLanguages supportedLanguages; private final SupportedLanguages supportedLanguages;
private final Environment env; private final Environment env;
@@ -43,8 +41,7 @@ public class Cryptomator {
private final ShutdownHook shutdownHook; private final ShutdownHook shutdownHook;
@Inject @Inject
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, SupportedLanguages supportedLanguages, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, ShutdownHook shutdownHook) { Cryptomator(DebugMode debugMode, SupportedLanguages supportedLanguages, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, ShutdownHook shutdownHook) {
this.logConfig = logConfig;
this.debugMode = debugMode; this.debugMode = debugMode;
this.supportedLanguages = supportedLanguages; this.supportedLanguages = supportedLanguages;
this.env = env; this.env = env;
@@ -79,9 +76,9 @@ public class Cryptomator {
* @return Nonzero exit code in case of an error. * @return Nonzero exit code in case of an error.
*/ */
private int run(String[] args) { private int run(String[] args) {
logConfig.init(); env.log();
LOG.debug("Dagger graph initialized after {}ms", System.currentTimeMillis() - STARTUP_TIME); LOG.debug("Dagger graph initialized after {}ms", System.currentTimeMillis() - STARTUP_TIME);
LOG.info("Starting Cryptomator {} on {} {} ({})", env.getAppVersion().orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH); LOG.info("Starting Cryptomator {} on {} {} ({})", env.getAppVersion(), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
debugMode.initialize(); debugMode.initialize();
supportedLanguages.applyPreferred(); supportedLanguages.applyPreferred();
@@ -3,14 +3,13 @@ package org.cryptomator.launcher;
import dagger.BindsInstance; import dagger.BindsInstance;
import dagger.Component; import dagger.Component;
import org.cryptomator.common.CommonsModule; import org.cryptomator.common.CommonsModule;
import org.cryptomator.logging.LoggerModule;
import org.cryptomator.ui.fxapp.FxApplicationComponent; import org.cryptomator.ui.fxapp.FxApplicationComponent;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
@Singleton @Singleton
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class}) @Component(modules = {CryptomatorModule.class, CommonsModule.class})
public interface CryptomatorComponent { public interface CryptomatorComponent {
Cryptomator application(); Cryptomator application();
@@ -15,9 +15,9 @@ public class SupportedLanguages {
private static final Logger LOG = LoggerFactory.getLogger(SupportedLanguages.class); private static final Logger LOG = LoggerFactory.getLogger(SupportedLanguages.class);
// these are BCP 47 language codes, not ISO. Note the "-" instead of the "_": // these are BCP 47 language codes, not ISO. Note the "-" instead of the "_":
public static final List<String> LANGUAGAE_TAGS = List.of("en", "ar", "bn", "bs", "ca", "cs", "de", "el", "es", "fil", "fr", "gl", "he", // public static final List<String> LANGUAGAE_TAGS = List.of("en", "ar", "be", "bn", "bs", "ca", "cs", "da", "de", "el", "es", "fil", "fa", "fr", "gl", "he", //
"hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "mk", "nb", "nl", "nn", "no", "pa", "pl", "pt", "pt-BR", "ro", "ru", "sk", "sr", // "hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "mk", "nb", "nl", "nn", "no", "pa", "pl", "pt", "pt-BR", "ro", "ru", "si", "sk", "sr", "sr-Latn", "sv", "sw", //
"sr-Latn", "sv", "sw", "ta", "te", "th", "tr", "uk", "zh", "zh-HK", "zh-TW"); "ta", "te", "th", "tr", "uk", "vi", "zh", "zh-HK", "zh-TW");
@Nullable @Nullable
private final String preferredLanguage; private final String preferredLanguage;
@@ -5,29 +5,24 @@
*******************************************************************************/ *******************************************************************************/
package org.cryptomator.logging; package org.cryptomator.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import java.util.Map;
@Singleton @Singleton
public class DebugMode { public class DebugMode {
private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(DebugMode.class); private static final Logger LOG = LoggerFactory.getLogger(DebugMode.class);
private final Settings settings; private final Settings settings;
private final LoggerContext context;
@Inject @Inject
public DebugMode(Settings settings, LoggerContext context) { public DebugMode(Settings settings) {
this.settings = settings; this.settings = settings;
this.context = context;
} }
public void initialize() { public void initialize() {
@@ -40,19 +35,13 @@ public class DebugMode {
} }
private void setLogLevels(boolean debugMode) { private void setLogLevels(boolean debugMode) {
var configurator = LogbackConfiguratorFactory.provider();
if (debugMode) { if (debugMode) {
setLogLevels(LoggerModule.DEBUG_LOG_LEVELS); configurator.setLogLevels(LogbackConfigurator.DEBUG_LOG_LEVELS);
LOG.debug("Debug mode enabled"); LOG.debug("Debug mode enabled");
} else { } else {
LOG.debug("Debug mode disabled"); LOG.debug("Debug mode disabled");
setLogLevels(LoggerModule.DEFAULT_LOG_LEVELS); configurator.setLogLevels(LogbackConfigurator.DEFAULT_LOG_LEVELS);
}
}
private void setLogLevels(Map<String, Level> logLevels) {
for (Map.Entry<String, Level> loglevel : logLevels.entrySet()) {
Logger logger = context.getLogger(loglevel.getKey());
logger.setLevel(loglevel.getValue());
} }
} }
@@ -0,0 +1,145 @@
package org.cryptomator.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.Configurator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.helpers.NOPAppender;
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.util.FileSize;
import org.cryptomator.common.Environment;
import java.nio.file.Path;
import java.util.Map;
public class LogbackConfigurator extends ContextAwareBase implements Configurator {
private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";
private static final String UPGRADE_FILENAME = "upgrade.log";
private static final String LOGFILE_NAME = "cryptomator0.log";
private static final String LOGFILE_ROLLING_PATTERN = "cryptomator%i.log";
private static final int LOGFILE_ROLLING_MIN = 1;
private static final int LOGFILE_ROLLING_MAX = 9;
private static final String LOG_MAX_SIZE = "100mb";
static final Map<String, Level> DEFAULT_LOG_LEVELS = Map.of( //
Logger.ROOT_LOGGER_NAME, Level.INFO, //
"org.cryptomator", Level.INFO //
);
static final Map<String, Level> DEBUG_LOG_LEVELS = Map.of( //
Logger.ROOT_LOGGER_NAME, Level.INFO, //
"org.cryptomator", Level.TRACE //
);
LogbackConfigurator() {}
/**
* Adjust the log levels
*
* @param logLevels new log levels to use
*/
void setLogLevels(Map<String, Level> logLevels) {
if (context instanceof LoggerContext lc) {
for (var loglevel : logLevels.entrySet()) {
Logger logger = lc.getLogger(loglevel.getKey());
logger.setLevel(loglevel.getValue());
}
}
}
@Override
public ExecutionStatus configure(LoggerContext context) {
var useCustomCfg = Environment.getInstance().useCustomLogbackConfig();
var logDir = Environment.getInstance().getLogDir().orElse(null);
if (useCustomCfg) {
addInfo("Using external logback configuration file.");
} else {
// configure appenders:
var stdout = stdOutAppender(context);
var noop = noopAppender(context);
var file = logDir == null ? noop : fileAppender(context, logDir);
var upgrade = logDir == null ? noop : upgradeAppender(context, logDir);
// configure loggers:
for (var loglevel : DEFAULT_LOG_LEVELS.entrySet()) {
Logger logger = context.getLogger(loglevel.getKey());
logger.setLevel(loglevel.getValue());
logger.setAdditive(false);
logger.addAppender(stdout);
logger.addAppender(file);
}
// configure upgrade logger:
Logger upgrades = context.getLogger("org.cryptomator.cryptofs.migration");
upgrades.setLevel(Level.DEBUG);
upgrades.addAppender(stdout);
upgrades.addAppender(upgrade);
upgrades.addAppender(file);
upgrades.setAdditive(false);
}
return ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY;
}
private Appender<ILoggingEvent> noopAppender(LoggerContext context) {
var appender = new NOPAppender<ILoggingEvent>();
appender.setContext(context);
return appender;
}
private Appender<ILoggingEvent> stdOutAppender(LoggerContext context) {
var appender = new ConsoleAppender<ILoggingEvent>();
appender.setContext(context);
appender.setName("STDOUT");
appender.setEncoder(encoder(context));
appender.start();
return appender;
}
private Appender<ILoggingEvent> upgradeAppender(LoggerContext context, Path logDir) {
var appender = new FileAppender<ILoggingEvent>();
appender.setContext(context);
appender.setName("UPGRADE");
appender.setFile(logDir.resolve(UPGRADE_FILENAME).toString());
appender.setEncoder(encoder(context));
appender.start();
return appender;
}
private Appender<ILoggingEvent> fileAppender(LoggerContext context, Path logDir) {
var appender = new RollingFileAppender<ILoggingEvent>();
appender.setContext(context);
appender.setName("FILE");
appender.setFile(logDir.resolve(LOGFILE_NAME).toString());
appender.setEncoder(encoder(context));
var triggeringPolicy = new LaunchAndSizeBasedTriggeringPolicy<ILoggingEvent>(FileSize.valueOf(LOG_MAX_SIZE));
triggeringPolicy.setContext(context);
triggeringPolicy.start();
appender.setTriggeringPolicy(triggeringPolicy);
var rollingPolicy = new FixedWindowRollingPolicy();
rollingPolicy.setContext(context);
rollingPolicy.setFileNamePattern(logDir.resolve(LOGFILE_ROLLING_PATTERN).toString());
rollingPolicy.setMinIndex(LOGFILE_ROLLING_MIN);
rollingPolicy.setMaxIndex(LOGFILE_ROLLING_MAX);
rollingPolicy.setParent(appender);
rollingPolicy.start();
appender.setRollingPolicy(rollingPolicy);
appender.start();
return appender;
}
private PatternLayoutEncoder encoder(LoggerContext context) {
var encoder = new PatternLayoutEncoder();
encoder.setContext(context);
encoder.setPattern(LOG_PATTERN);
encoder.start();
return encoder;
}
}
@@ -0,0 +1,12 @@
package org.cryptomator.logging;
public class LogbackConfiguratorFactory {
public static LogbackConfigurator provider() {
final class Holder {
private static final LogbackConfigurator INSTANCE = new LogbackConfigurator();
}
return Holder.INSTANCE;
}
}
@@ -1,70 +0,0 @@
package org.cryptomator.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import org.cryptomator.common.Environment;
import org.cryptomator.common.ShutdownHook;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Map;
@Singleton
public class LoggerConfiguration {
private final LoggerContext context;
private final Environment environment;
private final Appender<ILoggingEvent> stdout;
private final Appender<ILoggingEvent> upgrade;
private final Appender<ILoggingEvent> file;
private final ShutdownHook shutdownHook;
@Inject
LoggerConfiguration(LoggerContext context, //
Environment environment, //
@Named("stdoutAppender") Appender<ILoggingEvent> stdout, //
@Named("upgradeAppender") Appender<ILoggingEvent> upgrade, //
@Named("fileAppender") Appender<ILoggingEvent> file, //
ShutdownHook shutdownHook) {
this.context = context;
this.environment = environment;
this.stdout = stdout;
this.upgrade = upgrade;
this.file = file;
this.shutdownHook = shutdownHook;
}
public void init() {
if (environment.useCustomLogbackConfig()) {
Logger root = context.getLogger(Logger.ROOT_LOGGER_NAME);
root.info("Using external logback configuration file.");
} else {
context.reset();
// configure loggers:
for (Map.Entry<String, Level> loglevel : LoggerModule.DEFAULT_LOG_LEVELS.entrySet()) {
Logger logger = context.getLogger(loglevel.getKey());
logger.setLevel(loglevel.getValue());
logger.setAdditive(false);
logger.addAppender(stdout);
logger.addAppender(file);
}
// configure upgrade logger:
Logger upgrades = context.getLogger("org.cryptomator.cryptofs.migration");
upgrades.setLevel(Level.DEBUG);
upgrades.addAppender(stdout);
upgrades.addAppender(upgrade);
upgrades.addAppender(file);
upgrades.setAdditive(false);
// add shutdown hook
shutdownHook.runOnShutdown(ShutdownHook.PRIO_LAST, context::stop);
}
}
}
@@ -1,130 +0,0 @@
package org.cryptomator.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.helpers.NOPAppender;
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.util.FileSize;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.Environment;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.util.Map;
@Module
public class LoggerModule {
private static final String UPGRADE_FILENAME = "upgrade.log";
private static final String LOGFILE_NAME = "cryptomator0.log";
private static final String LOGFILE_ROLLING_PATTERN = "cryptomator%i.log";
private static final int LOGFILE_ROLLING_MIN = 1;
private static final int LOGFILE_ROLLING_MAX = 9;
private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";
private static final String LOG_MAX_SIZE = "100mb";
static final Map<String, Level> DEFAULT_LOG_LEVELS = Map.of( //
Logger.ROOT_LOGGER_NAME, Level.INFO, //
"org.cryptomator", Level.INFO //
);
static final Map<String, Level> DEBUG_LOG_LEVELS = Map.of( //
Logger.ROOT_LOGGER_NAME, Level.INFO, //
"org.cryptomator", Level.TRACE //
);
@Provides
@Singleton
static LoggerContext provideLoggerContext() {
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
if (loggerFactory instanceof LoggerContext context) {
return context;
} else {
throw new IllegalStateException("SLF4J not bound to Logback.");
}
}
@Provides
@Singleton
static PatternLayoutEncoder provideLayoutEncoder(LoggerContext context) {
PatternLayoutEncoder ple = new PatternLayoutEncoder();
ple.setPattern(LOG_PATTERN);
ple.setContext(context);
ple.start();
return ple;
}
@Provides
@Singleton
@Named("stdoutAppender")
static Appender<ILoggingEvent> provideStdoutAppender(LoggerContext context, PatternLayoutEncoder encoder) {
ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
appender.setContext(context);
appender.setEncoder(encoder);
appender.start();
return appender;
}
@Provides
@Singleton
@Named("fileAppender")
static Appender<ILoggingEvent> provideFileAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) {
var optionalLogDir = environment.getLogDir();
if (optionalLogDir.isPresent()) {
Path logDir = optionalLogDir.get();
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
appender.setContext(context);
appender.setFile(logDir.resolve(LOGFILE_NAME).toString());
appender.setEncoder(encoder);
LaunchAndSizeBasedTriggeringPolicy triggeringPolicy = new LaunchAndSizeBasedTriggeringPolicy(FileSize.valueOf(LOG_MAX_SIZE));
triggeringPolicy.setContext(context);
triggeringPolicy.start();
appender.setTriggeringPolicy(triggeringPolicy);
FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
rollingPolicy.setContext(context);
rollingPolicy.setFileNamePattern(logDir.resolve(LOGFILE_ROLLING_PATTERN).toString());
rollingPolicy.setMinIndex(LOGFILE_ROLLING_MIN);
rollingPolicy.setMaxIndex(LOGFILE_ROLLING_MAX);
rollingPolicy.setParent(appender);
rollingPolicy.start();
appender.setRollingPolicy(rollingPolicy);
appender.start();
return appender;
} else {
NOPAppender appender = new NOPAppender<>();
appender.setContext(context);
return appender;
}
}
@Provides
@Singleton
@Named("upgradeAppender")
static Appender<ILoggingEvent> provideUpgradeAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) {
var optionalLogDir = environment.getLogDir();
if (optionalLogDir.isPresent()) {
FileAppender<ILoggingEvent> appender = new FileAppender<>();
appender.setFile(optionalLogDir.get().resolve(UPGRADE_FILENAME).toString());
appender.setContext(context);
appender.setEncoder(encoder);
appender.start();
return appender;
} else {
NOPAppender appender = new NOPAppender<>();
appender.setContext(context);
return appender;
}
}
}
@@ -40,19 +40,15 @@ public class CreateNewVaultNameController implements FxController {
this.chooseLocationScene = chooseLocationScene; this.chooseLocationScene = chooseLocationScene;
this.vaultPath = vaultPath; this.vaultPath = vaultPath;
this.vaultName = vaultName; this.vaultName = vaultName;
this.validVaultName = Bindings.createBooleanBinding(this::isValidVaultNameInternal, vaultName); this.validVaultName = Bindings.createBooleanBinding(this::isValidVaultName, vaultName);
} }
@FXML @FXML
public void initialize() { public void initialize() {
vaultName.bind(textField.textProperty()); vaultName.bindBidirectional(textField.textProperty());
vaultName.addListener(this::vaultNameChanged); vaultName.addListener(this::vaultNameChanged);
} }
private boolean isValidVaultNameInternal() {
return vaultName.get() != null && VALID_NAME_PATTERN.matcher(vaultName.get().trim()).matches();
}
private void vaultNameChanged(@SuppressWarnings("unused") Observable observable) { private void vaultNameChanged(@SuppressWarnings("unused") Observable observable) {
if (isValidVaultName()) { if (isValidVaultName()) {
if (vaultPath.get() != null) { if (vaultPath.get() != null) {
@@ -70,6 +66,7 @@ public class CreateNewVaultNameController implements FxController {
@FXML @FXML
public void next() { public void next() {
window.setScene(chooseLocationScene.get()); window.setScene(chooseLocationScene.get());
vaultName.set(vaultName.get().trim());
} }
/* Getter/Setter */ /* Getter/Setter */
@@ -79,7 +76,7 @@ public class CreateNewVaultNameController implements FxController {
} }
public boolean isValidVaultName() { public boolean isValidVaultName() {
return validVaultName.get(); return vaultName.get() != null && VALID_NAME_PATTERN.matcher(vaultName.get().trim()).matches();
} }
} }
@@ -102,7 +102,7 @@ public class CreateNewVaultPasswordController implements FxController {
this.masterkeyFileAccess = masterkeyFileAccess; this.masterkeyFileAccess = masterkeyFileAccess;
this.processing = new SimpleBooleanProperty(); this.processing = new SimpleBooleanProperty();
this.readyToCreateVault = new SimpleBooleanProperty(); this.readyToCreateVault = new SimpleBooleanProperty();
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing); this.createVaultButtonState = Bindings.when(processing).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
} }
@FXML @FXML
@@ -231,6 +231,6 @@ public class CreateNewVaultPasswordController implements FxController {
} }
public ContentDisplay getCreateVaultButtonState() { public ContentDisplay getCreateVaultButtonState() {
return processing.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY; return createVaultButtonState.get();
} }
} }
@@ -12,15 +12,15 @@ import javafx.beans.value.ObservableValue;
* <p> * <p>
* During creation the consumer can optionally define actions to be executed everytime before the animation starts and after it stops. * During creation the consumer can optionally define actions to be executed everytime before the animation starts and after it stops.
*/ */
public class AutoAnimator<T extends Animation> { public class AutoAnimator {
private final T animation; private final Animation animation;
private final ObservableValue<Boolean> condition; private final ObservableValue<Boolean> condition;
private final Runnable beforeStart; private final Runnable beforeStart;
private final Runnable afterStop; private final Runnable afterStop;
private final Subscription sub; private final Subscription sub;
AutoAnimator(T animation, ObservableValue<Boolean> condition, Runnable beforeStart, Runnable afterStop) { AutoAnimator(Animation animation, ObservableValue<Boolean> condition, Runnable beforeStart, Runnable afterStop) {
this.animation = animation; this.animation = animation;
this.condition = condition; this.condition = condition;
this.beforeStart = beforeStart; this.beforeStart = beforeStart;
@@ -52,7 +52,7 @@ public class AutoAnimator<T extends Animation> {
public static class Builder { public static class Builder {
private Animation animation; private final Animation animation;
private ObservableValue<Boolean> condition = new SimpleBooleanProperty(true); private ObservableValue<Boolean> condition = new SimpleBooleanProperty(true);
private Runnable beforeStart = () -> {}; private Runnable beforeStart = () -> {};
private Runnable afterStop = () -> {}; private Runnable afterStop = () -> {};
@@ -77,7 +77,7 @@ public class ErrorController implements FxController {
var enhancedTemplate = String.format(REPORT_BODY_TEMPLATE, // var enhancedTemplate = String.format(REPORT_BODY_TEMPLATE, //
System.getProperty("os.name"), // System.getProperty("os.name"), //
System.getProperty("os.version"), // System.getProperty("os.version"), //
environment.getAppVersion().orElse("undefined"), // environment.getAppVersion(), //
environment.getBuildNumber().orElse("undefined")); environment.getBuildNumber().orElse("undefined"));
var body = URLEncoder.encode(enhancedTemplate, StandardCharsets.UTF_8); var body = URLEncoder.encode(enhancedTemplate, StandardCharsets.UTF_8);
application.getHostServices().showDocument(REPORT_URL_FORMAT.formatted(title, body)); application.getHostServices().showDocument(REPORT_URL_FORMAT.formatted(title, body));
@@ -14,6 +14,7 @@ public enum FxmlFile {
HEALTH_START("/fxml/health_start.fxml"), // HEALTH_START("/fxml/health_start.fxml"), //
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), // HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), // HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), //
HUB_LICENSE_EXCEEDED("/fxml/hub_license_exceeded.fxml"), //
HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), // HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), // HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), //
HUB_REGISTER_SUCCESS("/fxml/hub_register_success.fxml"), // HUB_REGISTER_SUCCESS("/fxml/hub_register_success.fxml"), //
@@ -1,25 +0,0 @@
package org.cryptomator.ui.common;
import dagger.Lazy;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import javax.inject.Inject;
import javafx.application.Application;
import java.nio.file.Path;
@FxApplicationScoped
public class HostServiceRevealer implements Volume.Revealer {
private final Lazy<Application> application;
@Inject
public HostServiceRevealer(Lazy<Application> application) {
this.application = application;
}
@Override
public void reveal(Path p) throws Volume.VolumeException {
application.get().getHostServices().showDocument(p.toUri().toString());
}
}
@@ -1,6 +1,5 @@
package org.cryptomator.ui.common; package org.cryptomator.ui.common;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.ui.controls.FontAwesome5IconView; import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.cryptomator.ui.controls.NiceSecurePasswordField;
@@ -42,7 +41,7 @@ public class NewPasswordController implements FxController {
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty())); passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty()));
passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength)); passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription)); passwordStrengthLabel.textProperty().bind(passwordStrength.map(strengthRater::getStrengthDescription));
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::passwordFieldsMatch, passwordField.textProperty(), reenterField.textProperty()); BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::passwordFieldsMatch, passwordField.textProperty(), reenterField.textProperty());
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty(); BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
@@ -1,16 +1,20 @@
package org.cryptomator.ui.common; package org.cryptomator.ui.common;
import org.cryptomator.common.vaults.LockNotCompletedException; import dagger.Lazy;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume; import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.integrations.mount.UnmountFailedException;
import org.cryptomator.ui.fxapp.FxApplicationScoped; import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
@@ -24,13 +28,13 @@ public class VaultService {
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class); private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
private final Lazy<Application> application;
private final ExecutorService executorService; private final ExecutorService executorService;
private final HostServiceRevealer vaultRevealer;
@Inject @Inject
public VaultService(ExecutorService executorService, HostServiceRevealer vaultRevealer) { public VaultService(Lazy<Application> application, ExecutorService executorService) {
this.application = application;
this.executorService = executorService; this.executorService = executorService;
this.vaultRevealer = vaultRevealer;
} }
public void reveal(Vault vault) { public void reveal(Vault vault) {
@@ -43,7 +47,7 @@ public class VaultService {
* @param vault The vault to reveal * @param vault The vault to reveal
*/ */
public Task<Vault> createRevealTask(Vault vault) { public Task<Vault> createRevealTask(Vault vault) {
Task<Vault> task = new RevealVaultTask(vault, vaultRevealer); Task<Vault> task = new RevealVaultTask(vault, application.get().getHostServices());
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName())); task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName()));
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException())); task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException()));
return task; return task;
@@ -106,22 +110,21 @@ public class VaultService {
private static class RevealVaultTask extends Task<Vault> { private static class RevealVaultTask extends Task<Vault> {
private final Vault vault; private final Vault vault;
private final Volume.Revealer revealer; private final HostServices hostServices;
/** public RevealVaultTask(Vault vault, HostServices hostServices) {
* @param vault The vault to lock
* @param revealer The object to use to show the vault content to the user.
*/
public RevealVaultTask(Vault vault, Volume.Revealer revealer) {
this.vault = vault; this.vault = vault;
this.revealer = revealer; this.hostServices = hostServices;
setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException())); setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException()));
} }
@Override @Override
protected Vault call() throws Volume.VolumeException { protected Vault call() {
vault.reveal(revealer); switch (vault.getMountPoint()) {
case null -> LOG.warn("Not currently mounted");
case Mountpoint.WithPath m -> hostServices.showDocument(m.uri().toString());
case Mountpoint.WithUri m -> LOG.info("Vault mounted at {}", m.uri()); // TODO show in UI?
}
return vault; return vault;
} }
} }
@@ -180,7 +183,7 @@ public class VaultService {
} }
@Override @Override
protected Vault call() throws Volume.VolumeException, LockNotCompletedException { protected Vault call() throws UnmountFailedException, IOException {
vault.lock(forced); vault.lock(forced);
return vault; return vault;
} }
@@ -46,7 +46,7 @@ public class NiceSecurePasswordField extends StackPane {
nonPrintableCharsIcon.managedProperty().bind(passwordField.containingNonPrintableCharsProperty()); nonPrintableCharsIcon.managedProperty().bind(passwordField.containingNonPrintableCharsProperty());
revealPasswordIcon.setGlyph(FontAwesome5Icon.EYE); revealPasswordIcon.setGlyph(FontAwesome5Icon.EYE);
revealPasswordIcon.glyphProperty().bind(Bindings.createObjectBinding(this::getRevealPasswordGlyph, revealPasswordButton.selectedProperty())); revealPasswordIcon.glyphProperty().bind(Bindings.when(revealPasswordButton.selectedProperty()).then(FontAwesome5Icon.EYE_SLASH).otherwise(FontAwesome5Icon.EYE));
revealPasswordIcon.setGlyphSize(ICON_SIZE); revealPasswordIcon.setGlyphSize(ICON_SIZE);
revealPasswordButton.setContentDisplay(ContentDisplay.LEFT); revealPasswordButton.setContentDisplay(ContentDisplay.LEFT);
@@ -61,10 +61,6 @@ public class NiceSecurePasswordField extends StackPane {
disabledProperty().addListener(this::disabledChanged); disabledProperty().addListener(this::disabledChanged);
} }
private FontAwesome5Icon getRevealPasswordGlyph() {
return revealPasswordButton.isSelected() ? FontAwesome5Icon.EYE_SLASH : FontAwesome5Icon.EYE;
}
private void disabledChanged(@SuppressWarnings("unused") Observable observable) { private void disabledChanged(@SuppressWarnings("unused") Observable observable) {
revealPasswordButton.setSelected(false); revealPasswordButton.setSelected(false);
} }
@@ -71,9 +71,11 @@ public class SecurePasswordField extends TextField {
} }
public void cut() { public void cut() {
//not implemented by design
} }
public void copy() { public void copy() {
//not implemented by design
} }
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
@@ -3,10 +3,9 @@ package org.cryptomator.ui.fxapp;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import org.cryptomator.common.ShutdownHook; import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume; import org.cryptomator.integrations.mount.UnmountFailedException;
import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.common.VaultService;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -18,6 +17,7 @@ import javafx.collections.ObservableList;
import java.awt.Desktop; import java.awt.Desktop;
import java.awt.desktop.QuitResponse; import java.awt.desktop.QuitResponse;
import java.awt.desktop.QuitStrategy; import java.awt.desktop.QuitStrategy;
import java.io.IOException;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.EventObject; import java.util.EventObject;
import java.util.Set; import java.util.Set;
@@ -128,10 +128,8 @@ public class FxApplicationTerminator {
if (vault.isUnlocked()) { if (vault.isUnlocked()) {
try { try {
vault.lock(true); vault.lock(true);
} catch (Volume.VolumeException e) { } catch (UnmountFailedException | IOException e) {
LOG.error("Failed to unmount vault " + vault.getPath(), e); LOG.error("Failed to unmount vault " + vault.getPath(), e);
} catch (LockNotCompletedException e) {
LOG.error("Failed to lock vault " + vault.getPath(), e);
} }
} }
} }
@@ -9,14 +9,12 @@ import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
import javafx.concurrent.Worker; import javafx.concurrent.Worker;
import javafx.concurrent.WorkerStateEvent; import javafx.concurrent.WorkerStateEvent;
import javafx.util.Duration; import javafx.util.Duration;
import java.util.Comparator; import java.util.Comparator;
import java.util.Optional;
@FxApplicationScoped @FxApplicationScoped
public class UpdateChecker { public class UpdateChecker {
@@ -25,8 +23,7 @@ public class UpdateChecker {
private static final Duration AUTOCHECK_DELAY = Duration.seconds(5); private static final Duration AUTOCHECK_DELAY = Duration.seconds(5);
private final Settings settings; private final Settings settings;
private final Optional<String> applicationVersion; private final String currentVersion;
private final StringProperty currentVersionProperty;
private final StringProperty latestVersionProperty; private final StringProperty latestVersionProperty;
private final Comparator<String> semVerComparator; private final Comparator<String> semVerComparator;
private final ScheduledService<String> updateCheckerService; private final ScheduledService<String> updateCheckerService;
@@ -34,11 +31,10 @@ public class UpdateChecker {
@Inject @Inject
UpdateChecker(Settings settings, Environment env, @Named("latestVersion") StringProperty latestVersionProperty, @Named("SemVer") Comparator<String> semVerComparator, ScheduledService<String> updateCheckerService) { UpdateChecker(Settings settings, Environment env, @Named("latestVersion") StringProperty latestVersionProperty, @Named("SemVer") Comparator<String> semVerComparator, ScheduledService<String> updateCheckerService) {
this.settings = settings; this.settings = settings;
this.applicationVersion = env.getAppVersion();
this.latestVersionProperty = latestVersionProperty; this.latestVersionProperty = latestVersionProperty;
this.semVerComparator = semVerComparator; this.semVerComparator = semVerComparator;
this.updateCheckerService = updateCheckerService; this.updateCheckerService = updateCheckerService;
this.currentVersionProperty = new SimpleStringProperty(applicationVersion.orElse("SNAPSHOT")); this.currentVersion = env.getAppVersion();
} }
public void automaticallyCheckForUpdatesIfEnabled() { public void automaticallyCheckForUpdatesIfEnabled() {
@@ -66,11 +62,10 @@ public class UpdateChecker {
} }
private void checkSucceeded(WorkerStateEvent event) { private void checkSucceeded(WorkerStateEvent event) {
String currentVersion = applicationVersion.orElse(null);
String latestVersion = updateCheckerService.getValue(); String latestVersion = updateCheckerService.getValue();
LOG.info("Current version: {}, lastest version: {}", currentVersion, latestVersion); LOG.info("Current version: {}, lastest version: {}", currentVersion, latestVersion);
if (currentVersion == null || semVerComparator.compare(currentVersion, latestVersion) < 0) { if (semVerComparator.compare(currentVersion, latestVersion) < 0) {
// update is available // update is available
latestVersionProperty.set(latestVersion); latestVersionProperty.set(latestVersion);
} else { } else {
@@ -92,8 +87,8 @@ public class UpdateChecker {
return latestVersionProperty; return latestVersionProperty;
} }
public ReadOnlyStringProperty currentVersionProperty() { public String getCurrentVersion() {
return currentVersionProperty; return currentVersion;
} }
} }
@@ -56,7 +56,7 @@ public abstract class UpdateCheckerModule {
@FxApplicationScoped @FxApplicationScoped
static HttpRequest provideCheckForUpdatesRequest(Environment env) { static HttpRequest provideCheckForUpdatesRequest(Environment env) {
String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", // String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", //
env.getAppVersion().orElse("SNAPSHOT"), // env.getAppVersion(), //
SystemUtils.OS_NAME, // SystemUtils.OS_NAME, //
SystemUtils.OS_VERSION, // SystemUtils.OS_VERSION, //
SystemUtils.OS_ARCH); // SystemUtils.OS_ARCH); //
@@ -3,12 +3,12 @@ package org.cryptomator.ui.health;
import com.tobiasdiez.easybind.EasyBind; import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.EasyObservableList; import com.tobiasdiez.easybind.EasyObservableList;
import com.tobiasdiez.easybind.Subscription; import com.tobiasdiez.easybind.Subscription;
import com.tobiasdiez.easybind.optional.OptionalBinding;
import org.cryptomator.cryptofs.health.api.DiagnosticResult; import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.beans.binding.Binding; import javafx.beans.binding.Binding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@@ -22,15 +22,15 @@ public class CheckDetailController implements FxController {
private final EasyObservableList<Result> results; private final EasyObservableList<Result> results;
private final ObjectProperty<Check> check; private final ObjectProperty<Check> check;
private final OptionalBinding<Check.CheckState> checkState; private final ObservableValue<Check.CheckState> checkState;
private final Binding<String> checkName; private final ObservableValue<String> checkName;
private final Binding<Boolean> checkRunning; private final BooleanExpression checkRunning;
private final Binding<Boolean> checkScheduled; private final BooleanExpression checkScheduled;
private final Binding<Boolean> checkFinished; private final BooleanExpression checkFinished;
private final Binding<Boolean> checkSkipped; private final BooleanExpression checkSkipped;
private final Binding<Boolean> checkSucceeded; private final BooleanExpression checkSucceeded;
private final Binding<Boolean> checkFailed; private final BooleanExpression checkFailed;
private final Binding<Boolean> checkCancelled; private final BooleanExpression checkCancelled;
private final Binding<Number> countOfWarnSeverity; private final Binding<Number> countOfWarnSeverity;
private final Binding<Number> countOfCritSeverity; private final Binding<Number> countOfCritSeverity;
private final Binding<Boolean> warnOrCritsExist; private final Binding<Boolean> warnOrCritsExist;
@@ -44,15 +44,15 @@ public class CheckDetailController implements FxController {
this.resultListCellFactory = resultListCellFactory; this.resultListCellFactory = resultListCellFactory;
this.results = EasyBind.wrapList(FXCollections.observableArrayList()); this.results = EasyBind.wrapList(FXCollections.observableArrayList());
this.check = selectedTask; this.check = selectedTask;
this.checkState = EasyBind.wrapNullable(selectedTask).mapObservable(Check::stateProperty); this.checkState = selectedTask.flatMap(Check::stateProperty);
this.checkName = EasyBind.wrapNullable(selectedTask).map(Check::getName).orElse(""); this.checkName = selectedTask.map(Check::getName).orElse("");
this.checkRunning = checkState.map(Check.CheckState.RUNNING::equals).orElse(false); this.checkRunning = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.RUNNING::equals).orElse(false));
this.checkScheduled = checkState.map(Check.CheckState.SCHEDULED::equals).orElse(false); this.checkScheduled = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SCHEDULED::equals).orElse(false));
this.checkSkipped = checkState.map(Check.CheckState.SKIPPED::equals).orElse(false); this.checkSkipped =BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SKIPPED::equals).orElse(false));
this.checkSucceeded = checkState.map(Check.CheckState.SUCCEEDED::equals).orElse(false); this.checkSucceeded = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SUCCEEDED::equals).orElse(false));
this.checkFailed = checkState.map(Check.CheckState.ERROR::equals).orElse(false); this.checkFailed = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.ERROR::equals).orElse(false));
this.checkCancelled = checkState.map(Check.CheckState.CANCELLED::equals).orElse(false); this.checkCancelled = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.CANCELLED::equals).orElse(false));
this.checkFinished = EasyBind.combine(checkSucceeded, checkFailed, checkCancelled, (a, b, c) -> a || b || c); this.checkFinished = checkSucceeded.or(checkFailed).or(checkCancelled);
this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN)); this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN));
this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL)); this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL));
this.warnOrCritsExist = EasyBind.combine(checkSucceeded, countOfWarnSeverity, countOfCritSeverity, (suceeded, warns, crits) -> suceeded && (warns.longValue() > 0 || crits.longValue() > 0) ); this.warnOrCritsExist = EasyBind.combine(checkSucceeded, countOfWarnSeverity, countOfCritSeverity, (suceeded, warns, crits) -> suceeded && (warns.longValue() > 0 || crits.longValue() > 0) );
@@ -84,7 +84,7 @@ public class CheckDetailController implements FxController {
return checkName.getValue(); return checkName.getValue();
} }
public Binding<String> checkNameProperty() { public ObservableValue<String> checkNameProperty() {
return checkName; return checkName;
} }
@@ -108,7 +108,7 @@ public class CheckDetailController implements FxController {
return checkRunning.getValue(); return checkRunning.getValue();
} }
public Binding<Boolean> checkRunningProperty() { public BooleanExpression checkRunningProperty() {
return checkRunning; return checkRunning;
} }
@@ -116,7 +116,7 @@ public class CheckDetailController implements FxController {
return checkFinished.getValue(); return checkFinished.getValue();
} }
public Binding<Boolean> checkFinishedProperty() { public BooleanExpression checkFinishedProperty() {
return checkFinished; return checkFinished;
} }
@@ -124,7 +124,7 @@ public class CheckDetailController implements FxController {
return checkScheduled.getValue(); return checkScheduled.getValue();
} }
public Binding<Boolean> checkScheduledProperty() { public BooleanExpression checkScheduledProperty() {
return checkScheduled; return checkScheduled;
} }
@@ -132,7 +132,7 @@ public class CheckDetailController implements FxController {
return checkSkipped.getValue(); return checkSkipped.getValue();
} }
public Binding<Boolean> checkSkippedProperty() { public BooleanExpression checkSkippedProperty() {
return checkSkipped; return checkSkipped;
} }
@@ -140,7 +140,7 @@ public class CheckDetailController implements FxController {
return checkSucceeded.getValue(); return checkSucceeded.getValue();
} }
public Binding<Boolean> checkSucceededProperty() { public BooleanExpression checkSucceededProperty() {
return checkSucceeded; return checkSucceeded;
} }
@@ -148,7 +148,7 @@ public class CheckDetailController implements FxController {
return checkFailed.getValue(); return checkFailed.getValue();
} }
public Binding<Boolean> checkFailedProperty() { public BooleanExpression checkFailedProperty() {
return checkFailed; return checkFailed;
} }
@@ -164,7 +164,7 @@ public class CheckDetailController implements FxController {
return warnOrCritsExist.getValue(); return warnOrCritsExist.getValue();
} }
public Binding<Boolean> checkCancelledProperty() { public BooleanExpression checkCancelledProperty() {
return checkCancelled; return checkCancelled;
} }
@@ -1,21 +1,20 @@
package org.cryptomator.ui.health; package org.cryptomator.ui.health;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
public class CheckListCellController implements FxController { public class CheckListCellController implements FxController {
private final ObjectProperty<Check> check; private final ObjectProperty<Check> check;
private final Binding<String> checkName; private final ObservableValue<Boolean> checkRunnable;
private final Binding<Boolean> checkRunnable; private final ObservableValue<String> checkName;
/* FXML */ /* FXML */
public CheckBox checkbox; public CheckBox checkbox;
@@ -23,8 +22,8 @@ public class CheckListCellController implements FxController {
@Inject @Inject
public CheckListCellController() { public CheckListCellController() {
check = new SimpleObjectProperty<>(); check = new SimpleObjectProperty<>();
checkRunnable = EasyBind.wrapNullable(check).mapObservable(Check::stateProperty).map(Check.CheckState.RUNNABLE::equals).orElse(false); checkRunnable = check.flatMap(Check::stateProperty).map(Check.CheckState.RUNNABLE::equals).orElse(false);
checkName = EasyBind.wrapNullable(check).map(Check::getName).orElse(""); checkName = check.map(Check::getName).orElse("");
} }
public void initialize() { public void initialize() {
@@ -50,7 +49,7 @@ public class CheckListCellController implements FxController {
check.set(c); check.set(c);
} }
public Binding<String> checkNameProperty() { public ObservableValue<String> checkNameProperty() {
return checkName; return checkName;
} }
@@ -58,7 +57,7 @@ public class CheckListCellController implements FxController {
return checkName.getValue(); return checkName.getValue();
} }
public Binding<Boolean> checkRunnableProperty() { public ObservableValue<Boolean> checkRunnableProperty() {
return checkRunnable; return checkRunnable;
} }
@@ -13,13 +13,12 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
import javafx.util.Duration; import javafx.util.Duration;
@@ -38,10 +37,10 @@ public class ResultListCellController implements FxController {
private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class); private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class);
private final ObjectProperty<Result> result; private final ObjectProperty<Result> result;
private final ObservableObjectValue<DiagnosticResult.Severity> severity; private final ObservableValue<DiagnosticResult.Severity> severity;
private final Binding<String> description; private final ObservableValue<String> description;
private final ResultFixApplier fixApplier; private final ResultFixApplier fixApplier;
private final ObservableObjectValue<Result.FixState> fixState; private final ObservableValue<Result.FixState> fixState;
private final ObjectBinding<FontAwesome5Icon> severityGlyph; private final ObjectBinding<FontAwesome5Icon> severityGlyph;
private final ObjectBinding<FontAwesome5Icon> fixGlyph; private final ObjectBinding<FontAwesome5Icon> fixGlyph;
private final BooleanBinding fixable; private final BooleanBinding fixable;
@@ -62,10 +61,10 @@ public class ResultListCellController implements FxController {
@Inject @Inject
public ResultListCellController(ResultFixApplier fixApplier, ResourceBundle resourceBundle) { public ResultListCellController(ResultFixApplier fixApplier, ResourceBundle resourceBundle) {
this.result = new SimpleObjectProperty<>(null); this.result = new SimpleObjectProperty<>(null);
this.severity = EasyBind.wrapNullable(result).map(r -> r.diagnosis().getSeverity()).asOrdinary(); this.severity = result.map(Result::diagnosis).map(DiagnosticResult::getSeverity);
this.description = EasyBind.wrapNullable(result).map(Result::getDescription).orElse(""); this.description = result.map(Result::getDescription).orElse("");
this.fixApplier = fixApplier; this.fixApplier = fixApplier;
this.fixState = EasyBind.wrapNullable(result).mapObservable(Result::fixState).asOrdinary(); this.fixState = result.flatMap(Result::fixState);
this.severityGlyph = Bindings.createObjectBinding(this::getSeverityGlyph, result); this.severityGlyph = Bindings.createObjectBinding(this::getSeverityGlyph, result);
this.fixGlyph = Bindings.createObjectBinding(this::getFixGlyph, fixState); this.fixGlyph = Bindings.createObjectBinding(this::getFixGlyph, fixState);
this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState); this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState);
@@ -83,14 +82,15 @@ public class ResultListCellController implements FxController {
@FXML @FXML
public void initialize() { public void initialize() {
// see getGlyph() for relevant glyphs: // see getGlyph() for relevant glyphs:
subscriptions.addAll(List.of(EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-muted", Bindings.equal(severity, DiagnosticResult.Severity.INFO)), // subscriptions.addAll(List.of(EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-muted", severity.map(DiagnosticResult.Severity.INFO::equals).orElse(false)), //
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), // EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-primary", severity.map(DiagnosticResult.Severity.GOOD::equals).orElse(false)), //
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN)), // EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-orange", severity.map(DiagnosticResult.Severity.WARN::equals).orElse(false)), //
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-red", Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL)) // EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-red", severity.map(DiagnosticResult.Severity.CRITICAL::equals).orElse(false)) //
)); ));
var animation = Animations.createDiscrete360Rotation(fixView); var animation = Animations.createDiscrete360Rotation(fixView);
this.fixRunningRotator = AutoAnimator.animate(animation) // this.fixRunningRotator = AutoAnimator.animate(animation) //
.onCondition(Bindings.equal(fixState, Result.FixState.FIXING)) // .onCondition(fixing) //
.afterStop(() -> fixView.setRotate(0)) // .afterStop(() -> fixView.setRotate(0)) //
.build(); .build();
} }
@@ -127,7 +127,7 @@ public class ResultListCellController implements FxController {
return result; return result;
} }
public Binding<String> descriptionProperty() { public ObservableValue<String> descriptionProperty() {
return description; return description;
} }
@@ -173,7 +173,7 @@ public class ResultListCellController implements FxController {
} }
public boolean isFixable() { public boolean isFixable() {
return Result.FixState.FIXABLE.equals(fixState.get()); return Result.FixState.FIXABLE.equals(fixState.getValue());
} }
public BooleanBinding fixingProperty() { public BooleanBinding fixingProperty() {
@@ -181,7 +181,7 @@ public class ResultListCellController implements FxController {
} }
public boolean isFixing() { public boolean isFixing() {
return Result.FixState.FIXING.equals(fixState.get()); return Result.FixState.FIXING.equals(fixState.getValue());
} }
public BooleanBinding fixedProperty() { public BooleanBinding fixedProperty() {
@@ -189,7 +189,7 @@ public class ResultListCellController implements FxController {
} }
public boolean isFixed() { public boolean isFixed() {
return Result.FixState.FIXED.equals(fixState.get()); return Result.FixState.FIXED.equals(fixState.getValue());
} }
public BooleanBinding fixFailedProperty() { public BooleanBinding fixFailedProperty() {
@@ -197,7 +197,7 @@ public class ResultListCellController implements FxController {
} }
public Boolean isFixFailed() { public Boolean isFixFailed() {
return Result.FixState.FIX_FAILED.equals(fixState.get()); return Result.FixState.FIX_FAILED.equals(fixState.getValue());
} }
public BooleanBinding fixRunningOrDoneProperty() { public BooleanBinding fixRunningOrDoneProperty() {
@@ -56,8 +56,10 @@ public abstract class HubKeyLoadingModule {
@Named("deviceId") @Named("deviceId")
static String provideDeviceId(DeviceKey deviceKey) { static String provideDeviceId(DeviceKey deviceKey) {
var publicKey = Objects.requireNonNull(deviceKey.get()).getPublic().getEncoded(); var publicKey = Objects.requireNonNull(deviceKey.get()).getPublic().getEncoded();
var hashedKey = MessageDigestSupplier.SHA256.get().digest(publicKey); try (var instance = MessageDigestSupplier.SHA256.instance()) {
return BaseEncoding.base16().encode(hashedKey); var hashedKey = instance.get().digest(publicKey);
return BaseEncoding.base16().encode(hashedKey);
}
} }
@Provides @Provides
@@ -92,6 +94,13 @@ public abstract class HubKeyLoadingModule {
return fxmlLoaders.createScene(FxmlFile.HUB_AUTH_FLOW); return fxmlLoaders.createScene(FxmlFile.HUB_AUTH_FLOW);
} }
@Provides
@FxmlScene(FxmlFile.HUB_LICENSE_EXCEEDED)
@KeyLoadingScoped
static Scene provideLicenseExceededScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.HUB_LICENSE_EXCEEDED);
}
@Provides @Provides
@FxmlScene(FxmlFile.HUB_RECEIVE_KEY) @FxmlScene(FxmlFile.HUB_RECEIVE_KEY)
@KeyLoadingScoped @KeyLoadingScoped
@@ -139,6 +148,11 @@ public abstract class HubKeyLoadingModule {
return new NewPasswordController(resourceBundle, strengthRater); return new NewPasswordController(resourceBundle, strengthRater);
} }
@Binds
@IntoMap
@FxControllerKey(LicenseExceededController.class)
abstract FxController bindLicenseExceededController(LicenseExceededController controller);
@Binds @Binds
@IntoMap @IntoMap
@FxControllerKey(ReceiveKeyController.class) @FxControllerKey(ReceiveKeyController.class)
@@ -0,0 +1,23 @@
package org.cryptomator.ui.keyloading.hub;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.keyloading.KeyLoading;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
public class LicenseExceededController implements FxController {
private final Stage window;
@Inject
public LicenseExceededController(@KeyLoading Stage window) {
this.window = window;
}
@FXML
public void close() {
window.close();
}
}
@@ -8,8 +8,6 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.keyloading.KeyLoading; import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped; import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@@ -44,10 +42,11 @@ public class ReceiveKeyController implements FxController {
private final Lazy<Scene> registerDeviceScene; private final Lazy<Scene> registerDeviceScene;
private final Lazy<Scene> unauthorizedScene; private final Lazy<Scene> unauthorizedScene;
private final URI vaultBaseUri; private final URI vaultBaseUri;
private final Lazy<Scene> licenseExceededScene;
private final HttpClient httpClient; private final HttpClient httpClient;
@Inject @Inject
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<JWEObject> result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy<Scene> unauthorizedScene) { public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<JWEObject> result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy<Scene> unauthorizedScene, @FxmlScene(FxmlFile.HUB_LICENSE_EXCEEDED) Lazy<Scene> licenseExceededScene) {
this.window = window; this.window = window;
this.deviceId = deviceId; this.deviceId = deviceId;
this.bearerToken = Objects.requireNonNull(tokenRef.get()); this.bearerToken = Objects.requireNonNull(tokenRef.get());
@@ -55,8 +54,9 @@ public class ReceiveKeyController implements FxController {
this.registerDeviceScene = registerDeviceScene; this.registerDeviceScene = registerDeviceScene;
this.unauthorizedScene = unauthorizedScene; this.unauthorizedScene = unauthorizedScene;
this.vaultBaseUri = getVaultBaseUri(vault); this.vaultBaseUri = getVaultBaseUri(vault);
this.licenseExceededScene = licenseExceededScene;
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
this.httpClient = HttpClient.newBuilder().executor(executor).build(); this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
} }
@FXML @FXML
@@ -75,6 +75,7 @@ public class ReceiveKeyController implements FxController {
try { try {
switch (response.statusCode()) { switch (response.statusCode()) {
case 200 -> retrievalSucceeded(response); case 200 -> retrievalSucceeded(response);
case 402 -> licenseExceeded();
case 403 -> accessNotGranted(); case 403 -> accessNotGranted();
case 404 -> needsDeviceRegistration(); case 404 -> needsDeviceRegistration();
default -> throw new IOException("Unexpected response " + response.statusCode()); default -> throw new IOException("Unexpected response " + response.statusCode());
@@ -94,6 +95,10 @@ public class ReceiveKeyController implements FxController {
} }
} }
private void licenseExceeded() {
window.setScene(licenseExceededScene.get());
}
private void needsDeviceRegistration() { private void needsDeviceRegistration() {
window.setScene(registerDeviceScene.get()); window.setScene(registerDeviceScene.get());
} }
@@ -76,7 +76,7 @@ public class RegisterDeviceController implements FxController {
this.registerFailedScene = registerFailedScene; this.registerFailedScene = registerFailedScene;
this.jwt = JWT.decode(this.bearerToken); this.jwt = JWT.decode(this.bearerToken);
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
this.httpClient = HttpClient.newBuilder().executor(executor).build(); this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
} }
public void initialize() { public void initialize() {
@@ -50,7 +50,7 @@ public class PassphraseEntryController implements FxController {
private final KeychainManager keychain; private final KeychainManager keychain;
private final StringBinding vaultName; private final StringBinding vaultName;
private final BooleanProperty unlockInProgress = new SimpleBooleanProperty(); private final BooleanProperty unlockInProgress = new SimpleBooleanProperty();
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, unlockInProgress); private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.when(unlockInProgress).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
private final BooleanProperty unlockButtonDisabled = new SimpleBooleanProperty(); private final BooleanProperty unlockButtonDisabled = new SimpleBooleanProperty();
/* FXML */ /* FXML */
@@ -186,7 +186,7 @@ public class PassphraseEntryController implements FxController {
} }
public ContentDisplay getUnlockButtonContentDisplay() { public ContentDisplay getUnlockButtonContentDisplay() {
return unlockInProgress.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY; return unlockButtonContentDisplay.get();
} }
public ReadOnlyBooleanProperty userInteractionDisabledProperty() { public ReadOnlyBooleanProperty userInteractionDisabledProperty() {
@@ -53,7 +53,7 @@ public class LockForcedController implements FxController {
} }
public boolean isForceSupported() { public boolean isForceSupported() {
return vault.supportsForcedUnmount(); return false;//vault.supportsForcedUnmount(); TODO
} }
} }
@@ -1,10 +1,9 @@
package org.cryptomator.ui.lock; package org.cryptomator.ui.lock;
import dagger.Lazy; import dagger.Lazy;
import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume; import org.cryptomator.integrations.mount.UnmountFailedException;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.cryptomator.ui.fxapp.FxApplicationWindows;
@@ -17,6 +16,7 @@ import javafx.concurrent.Task;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.stage.Window; import javafx.stage.Window;
import java.io.IOException;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@@ -53,21 +53,21 @@ public class LockWorkflow extends Task<Void> {
} }
@Override @Override
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException, ExecutionException { protected Void call() throws InterruptedException, ExecutionException, IOException {
lock(false); lock(false);
return null; return null;
} }
private void lock(boolean forced) throws InterruptedException, ExecutionException { private void lock(boolean forced) throws InterruptedException, ExecutionException, IOException { //TODO: catch or rethrow IOException?
try { try {
vault.lock(forced); vault.lock(forced);
} catch (Volume.VolumeException | LockNotCompletedException e) { } catch (UnmountFailedException e) {
LOG.info("Locking {} failed (forced: {}).", vault.getDisplayName(), forced, e); LOG.info("Locking {} failed (forced: {}).", vault.getDisplayName(), forced, e);
retryOrCancel(); retryOrCancel();
} }
} }
private void retryOrCancel() throws ExecutionException, InterruptedException { private void retryOrCancel() throws ExecutionException, InterruptedException, IOException {
try { try {
boolean forced = askWhetherToUseTheForce().get(); boolean forced = askWhetherToUseTheForce().get();
lock(forced); lock(forced);
@@ -105,7 +105,7 @@ public class LockWorkflow extends Task<Void> {
final var throwable = super.getException(); final var throwable = super.getException();
LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable); LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
if (throwable instanceof Volume.VolumeException) { if (throwable instanceof UnmountFailedException) { //TODO: check if correct exception caught
lockWindow.setScene(lockFailedScene.get()); lockWindow.setScene(lockFailedScene.get());
lockWindow.show(); lockWindow.show();
} else { } else {
@@ -46,7 +46,7 @@ public class ResizeController implements FxController {
ResizeController(@MainWindow Stage window, Settings settings) { ResizeController(@MainWindow Stage window, Settings settings) {
this.window = window; this.window = window;
this.settings = settings; this.settings = settings;
this.showResizingArrows = Bindings.createBooleanBinding(this::isShowResizingArrows, window.fullScreenProperty()); this.showResizingArrows = window.fullScreenProperty().not();
} }
@FXML @FXML
@@ -181,8 +181,7 @@ public class ResizeController implements FxController {
} }
public boolean isShowResizingArrows() { public boolean isShowResizingArrows() {
//If in fullscreen resizing is not be possible; return showResizingArrows.get();
return !window.isFullScreen();
} }
} }
@@ -1,6 +1,5 @@
package org.cryptomator.ui.mainwindow; package org.cryptomator.ui.mainwindow;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.Animations;
@@ -11,10 +10,10 @@ import org.cryptomator.ui.controls.FontAwesome5IconView;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.application.Application; import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@MainWindowScoped @MainWindowScoped
@@ -22,7 +21,7 @@ public class VaultDetailController implements FxController {
private final ReadOnlyObjectProperty<Vault> vault; private final ReadOnlyObjectProperty<Vault> vault;
private final Application application; private final Application application;
private final Binding<FontAwesome5Icon> glyph; private final ObservableValue<FontAwesome5Icon> glyph;
private final BooleanBinding anyVaultSelected; private final BooleanBinding anyVaultSelected;
private AutoAnimator spinAnimation; private AutoAnimator spinAnimation;
@@ -35,15 +34,13 @@ public class VaultDetailController implements FxController {
VaultDetailController(ObjectProperty<Vault> vault, Application application) { VaultDetailController(ObjectProperty<Vault> vault, Application application) {
this.vault = vault; this.vault = vault;
this.application = application; this.application = application;
this.glyph = EasyBind.select(vault) // this.glyph = vault.flatMap(Vault::stateProperty).map(this::getGlyphForVaultState);
.selectObject(Vault::stateProperty) //
.map(this::getGlyphForVaultState);
this.anyVaultSelected = vault.isNotNull(); this.anyVaultSelected = vault.isNotNull();
} }
public void initialize() { public void initialize() {
this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) // this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) //
.onCondition(EasyBind.select(vault).selectObject(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals)) // .onCondition(vault.flatMap(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals).orElse(false)) //
.afterStop(() -> vaultStateView.setRotate(0)) // .afterStop(() -> vaultStateView.setRotate(0)) //
.build(); .build();
} }
@@ -77,7 +74,7 @@ public class VaultDetailController implements FxController {
return vault.get(); return vault.get();
} }
public Binding<FontAwesome5Icon> glyphProperty() { public ObservableValue<FontAwesome5Icon> glyphProperty() {
return glyph; return glyph;
} }
@@ -1,6 +1,5 @@
package org.cryptomator.ui.mainwindow; package org.cryptomator.ui.mainwindow;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
@@ -9,10 +8,10 @@ import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.stage.Stage; import javafx.stage.Stage;
@@ -24,7 +23,7 @@ public class VaultDetailLockedController implements FxController {
private final VaultOptionsComponent.Factory vaultOptionsWindow; private final VaultOptionsComponent.Factory vaultOptionsWindow;
private final KeychainManager keychain; private final KeychainManager keychain;
private final Stage mainWindow; private final Stage mainWindow;
private final BooleanExpression passwordSaved; private final ObservableValue<Boolean> passwordSaved;
@Inject @Inject
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultOptionsComponent.Factory vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) { VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultOptionsComponent.Factory vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
@@ -34,7 +33,7 @@ public class VaultDetailLockedController implements FxController {
this.keychain = keychain; this.keychain = keychain;
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
if (keychain.isSupported() && !keychain.isLocked()) { if (keychain.isSupported() && !keychain.isLocked()) {
this.passwordSaved = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(v -> keychain.getPassphraseStoredProperty(v.getId()))); this.passwordSaved = vault.flatMap(v -> keychain.getPassphraseStoredProperty(v.getId())).orElse(false);
} else { } else {
this.passwordSaved = new SimpleBooleanProperty(false); this.passwordSaved = new SimpleBooleanProperty(false);
} }
@@ -65,13 +64,11 @@ public class VaultDetailLockedController implements FxController {
return vault.get(); return vault.get();
} }
public BooleanExpression passwordSavedProperty() { public ObservableValue<Boolean> passwordSavedProperty() {
return passwordSaved; return passwordSaved;
} }
public boolean isPasswordSaved() { public boolean isPasswordSaved() {
if (keychain.isSupported() && vault.get() != null) { return passwordSaved.getValue();
return keychain.getPassphraseStoredProperty(vault.get().getId()).get();
} else return false;
} }
} }
@@ -4,16 +4,24 @@ import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader; import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache; import com.google.common.cache.LoadingCache;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.stats.VaultStatisticsComponent; import org.cryptomator.ui.stats.VaultStatisticsComponent;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.net.URI;
import java.util.Optional;
@MainWindowScoped @MainWindowScoped
public class VaultDetailUnlockedController implements FxController { public class VaultDetailUnlockedController implements FxController {
@@ -24,6 +32,10 @@ public class VaultDetailUnlockedController implements FxController {
private final Stage mainWindow; private final Stage mainWindow;
private final LoadingCache<Vault, VaultStatisticsComponent> vaultStats; private final LoadingCache<Vault, VaultStatisticsComponent> vaultStats;
private final VaultStatisticsComponent.Builder vaultStatsBuilder; private final VaultStatisticsComponent.Builder vaultStatsBuilder;
private final ObservableValue<Mountpoint> mountPoint;
private final ObservableValue<Boolean> accessibleViaPath;
private final ObservableValue<Boolean> accessibleViaUri;
private final ObservableValue<String> mountUri;
@Inject @Inject
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) { public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
@@ -33,6 +45,10 @@ public class VaultDetailUnlockedController implements FxController {
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats)); this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
this.vaultStatsBuilder = vaultStatsBuilder; this.vaultStatsBuilder = vaultStatsBuilder;
this.mountPoint = vault.flatMap(Vault::mountPointProperty);
this.accessibleViaPath = mountPoint.map(m -> m instanceof Mountpoint.WithPath).orElse(false);
this.accessibleViaUri = mountPoint.map(m -> m instanceof Mountpoint.WithUri).orElse(false);
this.mountUri = mountPoint.map(Mountpoint::uri).map(URI::toASCIIString).orElse("");
} }
private VaultStatisticsComponent buildVaultStats(Vault vault) { private VaultStatisticsComponent buildVaultStats(Vault vault) {
@@ -44,6 +60,11 @@ public class VaultDetailUnlockedController implements FxController {
vaultService.reveal(vault.get()); vaultService.reveal(vault.get());
} }
@FXML
public void copyMountUri() {
// TODO
}
@FXML @FXML
public void lock() { public void lock() {
appWindows.startLockWorkflow(vault.get(), mainWindow); appWindows.startLockWorkflow(vault.get(), mainWindow);
@@ -64,4 +85,29 @@ public class VaultDetailUnlockedController implements FxController {
return vault.get(); return vault.get();
} }
public ObservableValue<Boolean> accessibleViaPathProperty() {
return accessibleViaPath;
}
public boolean isAccessibleViaPath() {
return accessibleViaPath.getValue();
}
public ObservableValue<Boolean> accessibleViaUriProperty() {
return accessibleViaUri;
}
public boolean isAccessibleViaUri() {
return accessibleViaUri.getValue();
}
public ObservableValue<String> mountUriProperty() {
return mountUri;
}
public String getMountUri() {
return mountUri.getValue();
}
} }
@@ -1,6 +1,5 @@
package org.cryptomator.ui.mainwindow; package org.cryptomator.ui.mainwindow;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.Animations;
@@ -10,15 +9,15 @@ import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView; import org.cryptomator.ui.controls.FontAwesome5IconView;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
// unscoped because each cell needs its own controller // unscoped because each cell needs its own controller
public class VaultListCellController implements FxController { public class VaultListCellController implements FxController {
private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>(); private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private final Binding<FontAwesome5Icon> glyph; private final ObservableValue<FontAwesome5Icon> glyph;
private AutoAnimator spinAnimation; private AutoAnimator spinAnimation;
@@ -27,14 +26,12 @@ public class VaultListCellController implements FxController {
@Inject @Inject
VaultListCellController() { VaultListCellController() {
this.glyph = EasyBind.select(vault) // this.glyph = vault.flatMap(Vault::stateProperty).map(this::getGlyphForVaultState);
.selectObject(Vault::stateProperty) //
.map(this::getGlyphForVaultState);
} }
public void initialize() { public void initialize() {
this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) // this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) //
.onCondition(EasyBind.select(vault).selectObject(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals)) // .onCondition(vault.flatMap(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals).orElse(false)) //
.afterStop(() -> vaultStateView.setRotate(0)) // .afterStop(() -> vaultStateView.setRotate(0)) //
.build(); .build();
} }
@@ -55,7 +52,7 @@ public class VaultListCellController implements FxController {
/* Getter/Setter */ /* Getter/Setter */
public Binding<FontAwesome5Icon> glyphProperty() { public ObservableValue<FontAwesome5Icon> glyphProperty() {
return glyph; return glyph;
} }
@@ -1,8 +1,5 @@
package org.cryptomator.ui.mainwindow; package org.cryptomator.ui.mainwindow;
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.optional.ObservableOptionalValue;
import com.tobiasdiez.easybind.optional.OptionalBinding;
import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
@@ -14,33 +11,39 @@ import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Objects;
import static org.cryptomator.common.vaults.VaultState.Value.*; import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
import static org.cryptomator.common.vaults.VaultState.Value.LOCKED;
import static org.cryptomator.common.vaults.VaultState.Value.MISSING;
import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION;
import static org.cryptomator.common.vaults.VaultState.Value.UNLOCKED;
@MainWindowScoped @MainWindowScoped
public class VaultListContextMenuController implements FxController { public class VaultListContextMenuController implements FxController {
private final ObservableOptionalValue<Vault> selectedVault; private final ReadOnlyObjectProperty<Vault> selectedVault;
private final Stage mainWindow; private final Stage mainWindow;
private final FxApplicationWindows appWindows; private final FxApplicationWindows appWindows;
private final VaultService vaultService; private final VaultService vaultService;
private final KeychainManager keychain; private final KeychainManager keychain;
private final RemoveVaultComponent.Builder removeVault; private final RemoveVaultComponent.Builder removeVault;
private final VaultOptionsComponent.Factory vaultOptionsWindow; private final VaultOptionsComponent.Factory vaultOptionsWindow;
private final OptionalBinding<VaultState.Value> selectedVaultState; private final ObservableValue<VaultState.Value> selectedVaultState;
private final Binding<Boolean> selectedVaultPassphraseStored; private final ObservableValue<Boolean> selectedVaultPassphraseStored;
private final Binding<Boolean> selectedVaultRemovable; private final ObservableValue<Boolean> selectedVaultRemovable;
private final Binding<Boolean> selectedVaultUnlockable; private final ObservableValue<Boolean> selectedVaultUnlockable;
private final Binding<Boolean> selectedVaultLockable; private final ObservableValue<Boolean> selectedVaultLockable;
@Inject @Inject
VaultListContextMenuController(ObjectProperty<Vault> selectedVault, @MainWindow Stage mainWindow, FxApplicationWindows appWindows, VaultService vaultService, KeychainManager keychain, RemoveVaultComponent.Builder removeVault, VaultOptionsComponent.Factory vaultOptionsWindow) { VaultListContextMenuController(ObjectProperty<Vault> selectedVault, @MainWindow Stage mainWindow, FxApplicationWindows appWindows, VaultService vaultService, KeychainManager keychain, RemoveVaultComponent.Builder removeVault, VaultOptionsComponent.Factory vaultOptionsWindow) {
this.selectedVault = EasyBind.wrapNullable(selectedVault); this.selectedVault = selectedVault;
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
this.appWindows = appWindows; this.appWindows = appWindows;
this.vaultService = vaultService; this.vaultService = vaultService;
@@ -48,8 +51,8 @@ public class VaultListContextMenuController implements FxController {
this.removeVault = removeVault; this.removeVault = removeVault;
this.vaultOptionsWindow = vaultOptionsWindow; this.vaultOptionsWindow = vaultOptionsWindow;
this.selectedVaultState = this.selectedVault.mapObservable(Vault::stateProperty); this.selectedVaultState = selectedVault.flatMap(Vault::stateProperty).orElse(null);
this.selectedVaultPassphraseStored = this.selectedVault.map(this::isPasswordStored).orElse(false); this.selectedVaultPassphraseStored = selectedVault.map(this::isPasswordStored).orElse(false);
this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION)::contains).orElse(false); this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION)::contains).orElse(false);
this.selectedVaultUnlockable = selectedVaultState.map(LOCKED::equals).orElse(false); this.selectedVaultUnlockable = selectedVaultState.map(LOCKED::equals).orElse(false);
this.selectedVaultLockable = selectedVaultState.map(UNLOCKED::equals).orElse(false); this.selectedVaultLockable = selectedVaultState.map(UNLOCKED::equals).orElse(false);
@@ -61,40 +64,37 @@ public class VaultListContextMenuController implements FxController {
@FXML @FXML
public void didClickRemoveVault() { public void didClickRemoveVault() {
selectedVault.ifValuePresent(v -> { var vault = Objects.requireNonNull(selectedVault.get());
removeVault.vault(v).build().showRemoveVault(); removeVault.vault(vault).build().showRemoveVault();
});
} }
@FXML @FXML
public void didClickShowVaultOptions() { public void didClickShowVaultOptions() {
selectedVault.ifValuePresent(v -> { var vault = Objects.requireNonNull(selectedVault.get());
vaultOptionsWindow.create(v).showVaultOptionsWindow(SelectedVaultOptionsTab.ANY); vaultOptionsWindow.create(vault).showVaultOptionsWindow(SelectedVaultOptionsTab.ANY);
});
} }
@FXML @FXML
public void didClickUnlockVault() { public void didClickUnlockVault() {
selectedVault.ifValuePresent(v -> { var vault = Objects.requireNonNull(selectedVault.get());
appWindows.startUnlockWorkflow(v, mainWindow); appWindows.startUnlockWorkflow(vault, mainWindow);
});
} }
@FXML @FXML
public void didClickLockVault() { public void didClickLockVault() {
selectedVault.ifValuePresent(v -> { var vault = Objects.requireNonNull(selectedVault.get());
appWindows.startLockWorkflow(v, mainWindow); appWindows.startLockWorkflow(vault, mainWindow);
});
} }
@FXML @FXML
public void didClickRevealVault() { public void didClickRevealVault() {
selectedVault.ifValuePresent(vaultService::reveal); var vault = Objects.requireNonNull(selectedVault.get());
vaultService.reveal(vault);
} }
// Getter and Setter // Getter and Setter
public Binding<Boolean> selectedVaultUnlockableProperty() { public ObservableValue<Boolean> selectedVaultUnlockableProperty() {
return selectedVaultUnlockable; return selectedVaultUnlockable;
} }
@@ -102,7 +102,7 @@ public class VaultListContextMenuController implements FxController {
return selectedVaultUnlockable.getValue(); return selectedVaultUnlockable.getValue();
} }
public Binding<Boolean> selectedVaultLockableProperty() { public ObservableValue<Boolean> selectedVaultLockableProperty() {
return selectedVaultLockable; return selectedVaultLockable;
} }
@@ -110,7 +110,7 @@ public class VaultListContextMenuController implements FxController {
return selectedVaultLockable.getValue(); return selectedVaultLockable.getValue();
} }
public Binding<Boolean> selectedVaultRemovableProperty() { public ObservableValue<Boolean> selectedVaultRemovableProperty() {
return selectedVaultRemovable; return selectedVaultRemovable;
} }
@@ -118,7 +118,7 @@ public class VaultListContextMenuController implements FxController {
return selectedVaultRemovable.getValue(); return selectedVaultRemovable.getValue();
} }
public Binding<Boolean> selectedVaultPassphraseStoredProperty() { public ObservableValue<Boolean> selectedVaultPassphraseStoredProperty() {
return selectedVaultPassphraseStored; return selectedVaultPassphraseStored;
} }
@@ -69,11 +69,15 @@ public class VaultListController implements FxController {
} }
}); });
vaultList.addEventFilter(MouseEvent.MOUSE_RELEASED, this::deselect); vaultList.addEventFilter(MouseEvent.MOUSE_RELEASED, this::deselect);
//don't show context menu when no vault selected
vaultList.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, request -> { vaultList.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, request -> {
if (selectedVault.get() == null) { if (selectedVault.get() == null) {
request.consume(); request.consume();
} }
}); });
//show removeVaultDialog on certain key press
vaultList.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> { vaultList.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
if (keyEvent.getCode() == KeyCode.DELETE) { if (keyEvent.getCode() == KeyCode.DELETE) {
pressedShortcutToRemoveVault(); pressedShortcutToRemoveVault();
@@ -83,7 +83,7 @@ public class MigrationRunController implements FxController {
this.appWindows = appWindows; this.appWindows = appWindows;
this.startScene = startScene; this.startScene = startScene;
this.successScene = successScene; this.successScene = successScene;
this.migrateButtonContentDisplay = Bindings.createObjectBinding(this::getMigrateButtonContentDisplay, vault.stateProperty()); this.migrateButtonContentDisplay = Bindings.when(vault.processingProperty()).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
this.capabilityErrorScene = capabilityErrorScene; this.capabilityErrorScene = capabilityErrorScene;
this.migrationButtonDisabled = new SimpleBooleanProperty(); this.migrationButtonDisabled = new SimpleBooleanProperty();
this.migrationProgress = new SimpleDoubleProperty(volatileMigrationProgress); this.migrationProgress = new SimpleDoubleProperty(volatileMigrationProgress);
@@ -211,10 +211,7 @@ public class MigrationRunController implements FxController {
} }
public ContentDisplay getMigrateButtonContentDisplay() { public ContentDisplay getMigrateButtonContentDisplay() {
return switch (vault.getState()) { return migrateButtonContentDisplay.get();
case PROCESSING -> ContentDisplay.LEFT;
default -> ContentDisplay.TEXT_ONLY;
};
} }
public ReadOnlyDoubleProperty migrationProgressProperty() { public ReadOnlyDoubleProperty migrationProgressProperty() {

Some files were not shown because too many files have changed in this diff Show More