diff --git a/.crowdin.yml b/.crowdin.yml index a70a3975b..ab4470ffa 100644 --- a/.crowdin.yml +++ b/.crowdin.yml @@ -1,5 +1,6 @@ -commit_message: "[ci skip]" -escape_special_characters: 0 +commit_message: '[ci skip]' files: - source: /src/main/resources/i18n/strings.properties translation: /src/main/resources/i18n/strings_%two_letters_code%.properties + escape_quotes: 0 + escape_special_characters: 0 diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 3a7337fe3..83ec96635 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -22,7 +22,7 @@ include: Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or +* The use of sexual language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index 190104a47..000000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -name: "Bug Report" -about: "Create a report to help us improve" -labels: type:bug ---- - - - -### Description - -[Summarize your problem.] - -### System Setup - -* Operating system and version: [Windows/macOS/Linux + Version ( + Desktop Environment, if Linux)] -* Cryptomator version: [Shown in the settings] -* Volume type: [Dokany/FUSE/WebDAV, shown in the settings] - -### Steps to Reproduce - -1. [First step] -2. [Second step] -3. [and so on…] - -#### Expected Behavior - -[What you expect to happen.] - -#### Actual Behavior - -[What actually happens.] - -#### Reproducibility - -[Always/Intermittent/Only once] - -### Additional Information - -[Any additional information, log files, screenshots, configuration, or data that might be necessary to reproduce the issue.] - - diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 000000000..70bf9654f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,93 @@ +name: Bug Report +description: Create a report to help us improve +labels: ["type:bug"] +body: + - type: checkboxes + id: terms + attributes: + label: Please agree to the following + options: + - label: I have searched [existing issues](https://github.com/cryptomator/cryptomator/issues?q=) for duplicates + required: true + - label: I agree to follow this project's [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md) + required: true + - type: input + id: summary + attributes: + label: Summary + placeholder: Please summarize your problem. + validations: + required: true + - type: textarea + id: software-versions + attributes: + label: What software is involved? + description: | + Examples: + - Operating System: Windows 10 + - Cryptomator: 1.5.16 + - LibreOffice: 7.1.4 + value: | + - Operating System: + - Cryptomator: + - … + validations: + required: true + - type: dropdown + id: volume-type + attributes: + label: Volume Type + description: What is selected under Settings → Virtual Drive? + multiple: true + options: + - FUSE + - Dokany + - WebDAV + validations: + required: false + - type: textarea + id: reproduction-steps + attributes: + label: Steps to Reproduce + value: | + 1. [First Step] + 2. [Second Step] + 3. … + validations: + required: true + - type: textarea + id: expected-behaviour + attributes: + label: Expected Behavior + placeholder: What you expect to happen. + validations: + required: true + - type: textarea + id: actual-behaviour + attributes: + label: Actual Behavior + placeholder: What actually happens. + validations: + required: true + - type: dropdown + id: reproducibility + attributes: + label: Reproducibility + description: How often does the described behaviour occur? + options: + - Always + - Intermittent + - Only once + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant Log Output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: textarea + id: further-info + attributes: + label: Anything else? + description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue? \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md deleted file mode 100644 index 366f7ecca..000000000 --- a/.github/ISSUE_TEMPLATE/feature.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: "Feature Request" -about: "Suggest an idea for this project" -labels: type:feature-request ---- - - - -### Summary - -[One paragraph explanation of the feature.] - -### Motivation - -[Why are we doing this? What use cases does it support? What is the expected outcome?] - -### Considered Alternatives - -[A clear and concise description of the alternative solutions you've considered.] - -### Additional Context - -[Add any other context or screenshots about the feature request here.] diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 000000000..652f27234 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,37 @@ +name: Feature Request +description: Suggest an idea for this project +labels: ["type:feature-request"] +body: + - type: checkboxes + id: terms + attributes: + label: Please agree to the following + options: + - label: I have searched [existing issues](https://github.com/cryptomator/cryptomator/issues?q=) for duplicates + required: true + - label: I agree to follow this project's [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md) + required: true + - type: input + id: summary + attributes: + label: Summary + placeholder: Please summarize your feature request. + validations: + required: true + - type: textarea + id: motivation + attributes: + label: Motivation + description: Who requires this feature? What problem does the user face? How would this feature solve the problem? + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Considered Alternatives + description: What current alternatives or workarounds have you considered? Is there a different way to solve the same problem? + - type: textarea + id: context + attributes: + label: Anything else? + description: Any context, suggestions, screenshots, or concepts you want to share? \ No newline at end of file diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index c7dc14547..f9ef8460a 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -14,5 +14,5 @@ For _everything else_, please visit our official [Cryptomator Community](https:/ - Discussions about the apps - [Development discussions](https://community.cryptomator.org/c/development) - General questions - - Discussions regarding our design decissions + - Discussions regarding our design decisions - Our roadmap diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3cf0cb845..0e1f598be 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,27 +1,31 @@ name: Build on: - [push] + push: + +env: + JAVA_VERSION: 16 + +defaults: + run: + shell: bash jobs: - build: - name: Build and Test + test: + name: Compile and Test runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" steps: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: 16 - - uses: actions/cache@v1 + java-version: ${{ env.JAVA_VERSION }} + - uses: actions/cache@v2 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven- - - name: Ensure to use tagged version - run: mvn versions:set -DnewVersion=${GITHUB_REF##*/} # use shell parameter expansion to strip of 'refs/tags' - if: startsWith(github.ref, 'refs/tags/') - name: Build and Test run: mvn -B clean install jacoco:report -Pcoverage,dependency-check - name: Upload code coverage report @@ -29,102 +33,4 @@ jobs: run: bash <(curl -Ls https://coverage.codacy.com/get.sh) env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} - continue-on-error: true -### TODO: move to matrix build: - - name: Assemble buildkit-linux.zip - run: mvn -B clean package -DskipTests -Plinux - - name: Upload buildkit-linux.zip - uses: actions/upload-artifact@v1 - with: - name: buildkit-linux.zip - path: target/buildkit-linux.zip - - name: Assemble buildkit-mac.zip - run: mvn -B clean package -DskipTests -Pmac - - name: Upload buildkit-mac.zip - uses: actions/upload-artifact@v1 - with: - name: buildkit-mac.zip - path: target/buildkit-mac.zip - - name: Assemble buildkit-win.zip - run: mvn -B clean package -DskipTests -Pwindows - - name: Upload buildkit-win.zip - uses: actions/upload-artifact@v1 - with: - name: buildkit-win.zip - path: target/buildkit-win.zip - - release: - name: Draft a Release on GitHub Releases - runs-on: ubuntu-latest - needs: build - if: startsWith(github.ref, 'refs/tags/') && github.repository == 'cryptomator/cryptomator' - steps: - - name: Download buildkit-linux.zip - uses: actions/download-artifact@v1 - with: - name: buildkit-linux.zip - path: . - - name: Download buildkit-mac.zip - uses: actions/download-artifact@v1 - with: - name: buildkit-mac.zip - path: . - - name: Download buildkit-win.zip - uses: actions/download-artifact@v1 - with: - name: buildkit-win.zip - path: . - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - body: | - :construction: Work in Progress - - TODO: - * [ ] add Linux appimage, zsync file and signature file - * [ ] add Windows installer and signature file - * [ ] add MacOs disk image and signature file - - ## What's new - - ## Bugfixes - - ## Misc - - --- - - :scroll: A complete list of closed issues is available [here](LINK) - draft: true - prerelease: false - - name: Upload buildkit-linux.zip to GitHub Releases - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: buildkit-linux.zip - asset_name: buildkit-linux.zip - asset_content_type: application/zip - - name: Upload buildkit-mac.zip to GitHub Releases - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: buildkit-mac.zip - asset_name: buildkit-mac.zip - asset_content_type: application/zip - - name: Upload buildkit-win.zip to GitHub Releases - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: buildkit-win.zip - asset_name: buildkit-win.zip - asset_content_type: application/zip \ No newline at end of file + continue-on-error: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..c3e254426 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,529 @@ +name: Installers and Release + +on: + workflow_dispatch: + push: + tags: # see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet + - '[0-9]+.[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+-*' + +env: + JAVA_VERSION: 16 + +defaults: + run: + shell: bash + +jobs: + +# +# Buildkit +# + buildkit: + name: Build ${{ matrix.profile }}-buildkit + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + include: + - os: ubuntu-latest + profile: linux + - os: windows-latest + profile: win + - os: macos-latest + profile: mac + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: ${{ env.JAVA_VERSION }} + - uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Ensure to use tagged version + run: mvn versions:set -DnewVersion=${GITHUB_REF##*/} # use shell parameter expansion to strip of 'refs/tags' + if: startsWith(github.ref, 'refs/tags/') + - name: Build and Test + run: mvn -B clean package -Pdependency-check,${{ matrix.profile }} + - name: Patch buildkit + run: | + cp LICENSE.txt target + cp dist/${{ matrix.profile }}/launcher* target + cp target/cryptomator-*.jar target/mods + - name: Upload ${{ matrix.profile }}-buildkit + uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.profile }}-buildkit + path: | + target/libs + target/mods + target/LICENSE.txt + target/${{ matrix.launcher }} + if-no-files-found: error + +# +# Release Metadata +# + metadata: + name: Determine Version Metadata + runs-on: ubuntu-latest + outputs: + versionStr: ${{ steps.versions.outputs.versionStr }} + versionNum: ${{ steps.versions.outputs.versionNum }} + revNum: ${{ steps.versions.outputs.revNum }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - id: versions + run: | + if [[ $GITHUB_REF == refs/tags/* ]]; then + VERSION_NUM=`echo ${GITHUB_REF##*/} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` + echo "::set-output name=versionStr::${GITHUB_REF##*/}" + echo "::set-output name=versionNum::${VERSION_NUM}" + else + echo "::set-output name=versionStr::SNAPSHOT" + echo "::set-output name=versionNum::99.0.0" + fi + echo "::set-output name=revNum::`git rev-list --count HEAD`" + +# +# Application Directory +# + appdir: + name: Create ${{ matrix.profile }}-appdir + needs: [buildkit, metadata] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + include: + - os: ubuntu-latest + profile: linux + jpackageoptions: > + --app-version "${{ needs.metadata.outputs.versionNum }}.${{ needs.metadata.outputs.revNum }}" + --java-options "-Dfile.encoding=\"utf-8\"" + --java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\"" + --java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" + --java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" + --java-options "-Dcryptomator.showTrayIcon=false" + --java-options "-Dcryptomator.buildNumber=\"appimage-${{ needs.metadata.outputs.revNum }}\"" + --resource-dir dist/linux/resources + - os: windows-latest + profile: win + jpackageoptions: > + --app-version "${{ needs.metadata.outputs.versionNum }}.${{ needs.metadata.outputs.revNum }}" + --java-options "-Dfile.encoding=\"utf-8\"" + --java-options "-Dcryptomator.logDir=\"~/AppData/Roaming/Cryptomator\"" + --java-options "-Dcryptomator.settingsPath=\"~/AppData/Roaming/Cryptomator/settings.json\"" + --java-options "-Dcryptomator.ipcSocketPath=\"~/AppData/Roaming/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.keychainPath=\"~/AppData/Roaming/Cryptomator/keychain.json\"" + --java-options "-Dcryptomator.mountPointsDir=\"~/Cryptomator\"" + --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.buildNumber=\"msi-${{ needs.metadata.outputs.revNum }}\"" + --resource-dir dist/win/resources + --icon dist/win/resources/Cryptomator.ico + - os: macos-latest + profile: mac + jpackageoptions: > + --app-version "${{ needs.metadata.outputs.versionNum }}" + --java-options "-Dfile.encoding=\"utf-8\"" + --java-options "-Dcryptomator.logDir=\"~/Library/Logs/Cryptomator\"" + --java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\"" + --java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.metadata.outputs.revNum }}\"" + --mac-package-identifier org.cryptomator + --resource-dir dist/mac/resources + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: ${{ env.JAVA_VERSION }} + - name: Download ${{ matrix.profile }}-buildkit + uses: actions/download-artifact@v2 + with: + name: ${{ matrix.profile }}-buildkit + path: buildkit + - name: Create Runtime Image + run: > + ${JAVA_HOME}/bin/jlink + --verbose + --output runtime + --module-path "${JAVA_HOME}/jmods" + --add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility + --no-header-files + --no-man-pages + --strip-debug + --compress=1 + - name: Create App Directory + run: > + ${JAVA_HOME}/bin/jpackage + --verbose + --type app-image + --runtime-image runtime + --input buildkit/libs + --module-path buildkit/mods + --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator + --dest appdir + --name Cryptomator + --vendor "Skymatic GmbH" + --copyright "(C) 2016 - 2021 Skymatic GmbH" + --java-options "-Xss5m" + --java-options "-Xmx256m" + ${{ matrix.jpackageoptions }} + - name: Create appdir.tar + run: tar -cvf appdir.tar appdir + - name: Upload ${{ matrix.profile }}-appdir + uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.profile }}-appdir + path: appdir.tar + if-no-files-found: error + +# +# Linux Cryptomator.AppImage +# + linux-appimage: + name: Build Cryptomator.AppImage + runs-on: ubuntu-latest + needs: [appdir, metadata] + steps: + - uses: actions/checkout@v2 + - name: Download linux-appdir + uses: actions/download-artifact@v2 + with: + name: linux-appdir + - name: Untar appdir.tar + run: | + tar -xvf appdir.tar + - name: Patch Cryptomator.AppDir + run: | + mv appdir/Cryptomator Cryptomator.AppDir + cp -r dist/linux/appimage/resources/AppDir/* Cryptomator.AppDir/ + envsubst '${REVISION_NO}' < dist/linux/appimage/resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh + ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg + ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg + ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon + ln -s usr/share/applications/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/Cryptomator.desktop + ln -s bin/cryptomator.sh Cryptomator.AppDir/AppRun + env: + REVISION_NO: ${{ needs.metadata.outputs.revNum }} + - name: Extract libjffi.so # workaround for https://github.com/cryptomator/cryptomator-linux/issues/27 + run: | + JFFI_NATIVE_JAR=`ls lib/app/ | grep -e 'jffi-[1-9]\.[0-9]\{1,2\}.[0-9]\{1,2\}-native.jar'` + ${JAVA_HOME}/bin/jar -xf lib/app/${JFFI_NATIVE_JAR} /jni/x86_64-Linux/ + mv jni/x86_64-Linux/* lib/app/libjffi.so + working-directory: Cryptomator.AppDir + - name: Download AppImageKit + run: | + curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o appimagetool.AppImage + chmod +x appimagetool.AppImage + ./appimagetool.AppImage --appimage-extract + - name: Prepare GPG-Agent for signing with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign Cryptomator.AppDir/AppRun + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Build AppImage + run: > + ./squashfs-root/AppRun Cryptomator.AppDir cryptomator-${{ needs.metadata.outputs.versionStr }}-x86_64.AppImage + -u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync' + --sign --sign-key=615D449FE6E6A235 --sign-args="--batch --pinentry-mode loopback" + - name: Upload AppImage + uses: actions/upload-artifact@v2 + with: + name: linux-appimage + path: | + cryptomator-*.AppImage + cryptomator-*.AppImage.zsync + if-no-files-found: error + +# +# macOS Cryptomator.app +# + mac-app: + name: Build Cryptomator.app + runs-on: macos-latest + needs: [appdir, metadata] + steps: + - uses: actions/checkout@v2 + - name: Download mac-appdir + uses: actions/download-artifact@v2 + with: + name: mac-appdir + - name: Untar appdir.tar + run: tar -xvf appdir.tar + - name: Patch Cryptomator.app + run: | + mv appdir/Cryptomator.app Cryptomator.app + sed -i '' "s|###BUNDLE_SHORT_VERSION_STRING###|${VERSION_NO}|g" Cryptomator.app/Contents/Info.plist + sed -i '' "s|###BUNDLE_VERSION###|${REVISION_NO}|g" Cryptomator.app/Contents/Info.plist + env: + VERSION_NO: ${{ needs.metadata.outputs.versionNum }} + REVISION_NO: ${{ needs.metadata.outputs.revNum }} + - name: Install codesign certificate + env: + CODESIGN_P12_BASE64: ${{ secrets.MACOS_CODESIGN_P12_BASE64 }} + CODESIGN_P12_PW: ${{ secrets.MACOS_CODESIGN_P12_PW }} + CODESIGN_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_CODESIGN_TMP_KEYCHAIN_PW }} + run: | + # create variables + CERTIFICATE_PATH=$RUNNER_TEMP/codesign.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/codesign.keychain-db + + # import certificate and provisioning profile from secrets + echo -n "$CODESIGN_P12_BASE64" | base64 --decode --output $CERTIFICATE_PATH + + # create temporary keychain + security create-keychain -p "$CODESIGN_TMP_KEYCHAIN_PW" $KEYCHAIN_PATH + security set-keychain-settings -lut 900 $KEYCHAIN_PATH + security unlock-keychain -p "$CODESIGN_TMP_KEYCHAIN_PW" $KEYCHAIN_PATH + + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$CODESIGN_P12_PW" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + - name: Codesign + env: + CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} + run: | + find Cryptomator.app/Contents/runtime/Contents/MacOS -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; + for JAR_PATH in `find Cryptomator.app -name "*.jar"`; do + if [[ `unzip -l ${JAR_PATH} | grep '.dylib\|.jnilib'` ]]; then + JAR_FILENAME=$(basename ${JAR_PATH}) + OUTPUT_PATH=${JAR_PATH%.*} + echo "Codesigning libs in ${JAR_FILENAME}..." + unzip -q ${JAR_PATH} -d ${OUTPUT_PATH} + find ${OUTPUT_PATH} -name '*.dylib' -exec codesign -s ${CODESIGN_IDENTITY} {} \; + find ${OUTPUT_PATH} -name '*.jnilib' -exec codesign -s ${CODESIGN_IDENTITY} {} \; + rm ${JAR_PATH} + pushd ${OUTPUT_PATH} > /dev/null + zip -qr ../${JAR_FILENAME} * + popd > /dev/null + rm -r ${OUTPUT_PATH} + fi + done + echo "Codesigning Cryptomator.app..." + codesign --force --deep --entitlements dist/mac/Cryptomator.entitlements -o runtime -s ${CODESIGN_IDENTITY} Cryptomator.app + - name: Clean up codesign certificate + if: ${{ always() }} + run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db + - name: Create app.tar + run: tar -cvf app.tar Cryptomator.app + - name: Upload mac-app + uses: actions/upload-artifact@v2 + with: + name: mac-app + path: app.tar + if-no-files-found: error + +# +# macOS Cryptomator.dmg +# + mac-dmg: + name: Build Cryptomator.dmg + runs-on: macos-11 + needs: [mac-app, metadata] + steps: + - uses: actions/checkout@v2 + - name: Download mac-appdir + uses: actions/download-artifact@v2 + with: + name: mac-app + - name: Untar app.tar + run: tar -xvf app.tar + - name: Prepare .dmg contents + run: | + mkdir dmg + mv Cryptomator.app dmg + cp dist/mac/dmg/resources/macFUSE.webloc dmg + ls -l dmg + - name: Install create-dmg + run: | + brew install create-dmg + create-dmg --help + - name: Create .dmg + run: > + create-dmg + --volname Cryptomator + --volicon "dist/mac/dmg/resources/Cryptomator-Volume.icns" + --background "dist/mac/dmg/resources/Cryptomator-background.tiff" + --window-pos 400 100 + --window-size 640 694 + --icon-size 128 + --icon "Cryptomator.app" 128 245 + --hide-extension "Cryptomator.app" + --icon "macFUSE.webloc" 320 501 + --hide-extension "macFUSE.webloc" + --app-drop-link 512 245 + --eula "dist/mac/dmg/resources/license.rtf" + --icon ".background" 128 758 + --icon ".fseventsd" 320 758 + --icon ".VolumeIcon.icns" 512 758 + Cryptomator-${VERSION_NO}.dmg dmg + env: + VERSION_NO: ${{ needs.metadata.outputs.versionNum }} + - name: Install notarization credentials + env: + NOTARIZATION_KEYCHAIN_PROFILE: ${{ secrets.MACOS_NOTARIZATION_KEYCHAIN_PROFILE }} + NOTARIZATION_APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }} + NOTARIZATION_PW: ${{ secrets.MACOS_NOTARIZATION_PW }} + NOTARIZATION_TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }} + NOTARIZATION_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_NOTARIZATION_TMP_KEYCHAIN_PW }} + run: | + # create temporary keychain + KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db + security create-keychain -p "${NOTARIZATION_TMP_KEYCHAIN_PW}" ${KEYCHAIN_PATH} + security set-keychain-settings -lut 900 ${KEYCHAIN_PATH} + security unlock-keychain -p "${NOTARIZATION_TMP_KEYCHAIN_PW}" ${KEYCHAIN_PATH} + + # import credentials from secrets + sudo xcode-select -s /Applications/Xcode_13.0.app + xcrun notarytool store-credentials "${NOTARIZATION_KEYCHAIN_PROFILE}" --apple-id "${NOTARIZATION_APPLE_ID}" --password "${NOTARIZATION_PW}" --team-id "${NOTARIZATION_TEAM_ID}" --keychain "${KEYCHAIN_PATH}" + - name: Notarize .dmg + env: + NOTARIZATION_KEYCHAIN_PROFILE: ${{ secrets.MACOS_NOTARIZATION_KEYCHAIN_PROFILE }} + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db + sudo xcode-select -s /Applications/Xcode_13.0.app + xcrun notarytool submit Cryptomator-*.dmg --keychain-profile "${NOTARIZATION_KEYCHAIN_PROFILE}" --keychain "${KEYCHAIN_PATH}" --wait + xcrun stapler staple Cryptomator-*.dmg + - name: Clean up notarization credentials + if: ${{ always() }} + run: security delete-keychain $RUNNER_TEMP/notarization.keychain-db + - name: Upload mac-dmg + uses: actions/upload-artifact@v2 + with: + name: mac-dmg + path: Cryptomator-*.dmg + if-no-files-found: error + +# +# MSI package +# + win-msi: + name: Build Cryptomator.msi + runs-on: windows-latest + needs: [appdir, metadata] + steps: + - uses: actions/checkout@v2 + - name: Download win-appdir + uses: actions/download-artifact@v2 + with: + name: win-appdir + - name: Untar appdir.tar + run: tar -xvf appdir.tar + - uses: actions/setup-java@v1 + with: + java-version: ${{ env.JAVA_VERSION }} + - name: Patch Application Directory + run: | + cp dist/win/contrib/* appdir/Cryptomator + - name: Fix permissions + run: attrib -r appdir/Cryptomator/Cryptomator.exe + shell: pwsh + - name: Codesign + uses: skymatic/code-sign-action@v1 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B + description: Cryptomator + timestampUrl: 'http://timestamp.digicert.com' + folder: appdir/Cryptomator + recursive: true + - name: Create MSI + run: > + ${JAVA_HOME}/bin/jpackage + --verbose + --type msi + --win-upgrade-uuid bda45523-42b1-4cae-9354-a45475ed4775 + --app-image appdir/Cryptomator + --dest installer + --name Cryptomator + --vendor "Skymatic GmbH" + --copyright "(C) 2016 - 2021 Skymatic GmbH" + --app-version "${{ needs.metadata.outputs.versionNum }}" + --win-menu + --win-dir-chooser + --resource-dir dist/win/resources + --license-file dist/win/resources/license.rtf + --file-associations dist/win/resources/FAencryptedData.properties + --file-associations dist/win/resources/FAvaultFile.properties + env: + JP_WIXWIZARD_RESOURCES: ${{ github.workspace }}/dist/win/resources # requires abs path, used in resources/main.wxs + - name: Codesign MSI + uses: skymatic/code-sign-action@v1 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B + description: Cryptomator Installer + timestampUrl: 'http://timestamp.digicert.com' + folder: installer + - name: Upload win-msi + uses: actions/upload-artifact@v2 + with: + name: win-msi + path: installer/*.msi + if-no-files-found: error + +# +# Release +# + release: + name: Draft a release on Github + runs-on: ubuntu-latest + needs: [metadata,linux-appimage,mac-dmg,win-msi] + if: startsWith(github.ref, 'refs/tags/') && github.repository == 'cryptomator/cryptomator' + steps: + - uses: actions/checkout@v2 + - name: Create tarball + run: git archive --prefix="cryptomator-${{ needs.metadata.outputs.versionStr }}/" -o "cryptomator-${{ needs.metadata.outputs.versionStr }}.tar.gz" ${{ github.ref }} + - name: Download linux appimage + uses: actions/download-artifact@v2 + with: + name: linux-appimage + - name: Download macOS dmg + uses: actions/download-artifact@v2 + with: + name: mac-dmg + - name: Download Windows msi + uses: actions/download-artifact@v2 + with: + name: win-msi + - name: Create detached GPG signature for all release files with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + for FILE in `find . -name "*.AppImage" -o -name "*.dmg" -o -name "*.msi" -o -name "*.zsync" -o -name "*.tar.gz"`; do + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a ${FILE} + done + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Create release draft + uses: softprops/action-gh-release@v1 + with: + draft: true + fail_on_unmatched_files: true + discussion_category_name: releases + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + *.AppImage + *.zsync + *.asc + *.dmg + *.msi + body: | + :construction: Work in Progress + ## What's new + ## Bugfixes + ## Misc + --- + :scroll: A complete list of closed issues is available [here](LINK) diff --git a/.github/workflows/triageBugs.yml b/.github/workflows/triageBugs.yml deleted file mode 100644 index bcb0695b0..000000000 --- a/.github/workflows/triageBugs.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Bug Report Triage - -on: - issues: - types: [opened] - -jobs: - closeTemplateViolation: - name: Validate bug report against issue template - runs-on: ubuntu-latest - if: contains(github.event.issue.labels.*.name, 'type:bug') - steps: - - name: Check "Description" - if: | - !contains(github.event.issue.body, env.MUST_CONTAIN) - || contains(toJson(github.event.issue.body), env.MUST_NOT_CONTAIN) - run: exit 1 - env: - MUST_CONTAIN: '### Description' - MUST_NOT_CONTAIN: '### Description\r\n\r\n[Summarize your problem.]\r\n\r\n### System Setup' - - name: Check "Steps to Reproduce" - if: | - !contains(github.event.issue.body, env.MUST_CONTAIN) - || contains(toJson(github.event.issue.body), env.MUST_NOT_CONTAIN) - run: exit 1 - env: - MUST_CONTAIN: '### Steps to Reproduce' - MUST_NOT_CONTAIN: '### Steps to Reproduce\r\n\r\n1. [First step]\r\n2. [Second step]\r\n3. [and so on…]\r\n\r\n#### Expected Behavior' - - name: Close issue if one of the checks failed - if: ${{ failure() }} - uses: peter-evans/close-issue@v1 - with: - comment: | - This bug report did ignore our issue template. 😞 - Auto-closing this issue, since it is most likely not useful. - - _This decision was made by a bot. If you think the bot is wrong, let us know and we'll reopen this issue._ diff --git a/.gitignore b/.gitignore index d0e6b59a3..be67207df 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,9 @@ pom.xml.versionsBackup # IntelliJ Settings Files (https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems) # .idea/**/workspace.xml .idea/**/tasks.xml -.idea/dictionaries +.idea/**/shelf +.idea/dictionaries/** +!.idea/dictionaries/dict_* .idea/compiler.xml .idea/encodings.xml .idea/jarRepositories.xml diff --git a/.idea/dictionaries/dict_de.xml b/.idea/dictionaries/dict_de.xml new file mode 100644 index 000000000..a6ecf3c80 --- /dev/null +++ b/.idea/dictionaries/dict_de.xml @@ -0,0 +1,15 @@ + + + + tresorinhalt + tresorkonfigurationsdatei + tresorlaufwerk + tresorliste + tresorname + tresoroptionen + tresorstatistik + ungespeicherten + ungespeicherter + + + \ No newline at end of file diff --git a/.idea/dictionaries/dict_project.xml b/.idea/dictionaries/dict_project.xml new file mode 100644 index 000000000..5649810c7 --- /dev/null +++ b/.idea/dictionaries/dict_project.xml @@ -0,0 +1,106 @@ + + + + addvault + addvaultwizard + adoptopenjdk + affero + aquafx + autolock + autolocked + autolocking + baos + bkup + buildkit + changepassword + checkerframework + crit + crowdin + cryptofs + cryptomator + cryptomator's + cryptor + csprng + dcryptomator + dfuse + dirid + djdk + dokany + dragboard + easybind + encr + errorprone + failureaccess + fbidis + fldinst + fldrslt + fontawesomefx + gdrive + gvfs + hmmss + httpcomponents + httpcore + iclouddrive + jensd + jffi + keyloading + kibs + listcell + listenablefuture + lopp + mainwindow + masterkey + masterkeyfile + mibs + mountpath + mpc's + needsmigration + noncommercially + noël + nulab + oatomic + oauto + objectgraph + odefault + ogid + onoappledouble + ordonly + ouid + ovolname + passthrough + patreon + pcloud + probot + recoverykey + relicensing + removevault + revealer + serceman + setolabs + skymatic + socio + spof + stenzel + styleclass + sublicenses + sublicensing + systemkeychain + tada + tidelift + tmpmountpoint + tobiasdiez + toggler + traymenu + unknownerror + unlockable + vaultconfig + vaultlist + vaultname + vaultoptions + wrongfilealert + xattr + zillmann + zxcvbn + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 146ab09b7..0634be039 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,7 +1,7 @@ @@ -492,52 +458,10 @@ ${cryptomator.integrations.linux.version} - - - - org.apache.maven.plugins - maven-assembly-plugin - - - assemble-linux - package - - single - - - - assembly-linux.xml - - false - buildkit-linux - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-linux-libs - prepare-package - - copy-dependencies - - - ${project.build.directory}/libs - org.openjfx - linux - - - - - - - windows + win windows @@ -553,48 +477,6 @@ ${cryptomator.integrations.win.version} - - - - org.apache.maven.plugins - maven-assembly-plugin - - - assemble-win - package - - single - - - - assembly-win.xml - - false - buildkit-win - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-win-libs - prepare-package - - copy-dependencies - - - ${project.build.directory}/libs - org.openjfx - win - - - - - - diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 000000000..6ba69a6ef --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,59 @@ +import org.cryptomator.integrations.autostart.AutoStartProvider; +import org.cryptomator.integrations.keychain.KeychainAccessProvider; +import org.cryptomator.integrations.tray.TrayIntegrationProvider; +import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; + +module org.cryptomator.desktop { + requires org.cryptomator.cryptofs; + requires org.cryptomator.frontend.dokany; + requires org.cryptomator.frontend.fuse; + requires org.cryptomator.frontend.webdav; + requires org.cryptomator.integrations.api; + requires java.desktop; + requires java.net.http; + requires javafx.base; + requires javafx.graphics; + requires javafx.controls; + requires javafx.fxml; + requires com.tobiasdiez.easybind; + requires com.google.common; + requires com.google.gson; + requires com.nulabinc.zxcvbn; + requires org.slf4j; + requires org.apache.commons.lang3; + requires dagger; + requires com.auth0.jwt; + + /* TODO: filename-based modules: */ + requires static javax.inject; /* ugly dagger/guava crap */ + requires logback.classic; + requires logback.core; + + uses AutoStartProvider; + uses KeychainAccessProvider; + uses TrayIntegrationProvider; + uses UiAppearanceProvider; + + opens org.cryptomator.common.settings to com.google.gson; + + 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.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; +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/common/CommonsModule.java b/src/main/java/org/cryptomator/common/CommonsModule.java index 705f98b8f..eb7e6cd13 100644 --- a/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/src/main/java/org/cryptomator/common/CommonsModule.java @@ -46,6 +46,7 @@ public abstract class CommonsModule { private static final int NUM_CORE_BG_THREADS = 6; private static final long BG_THREAD_KEEPALIVE_SECONDS = 60l; + @SuppressWarnings("SpellCheckingInspection") @Provides @Singleton @Named("licensePublicKey") diff --git a/src/main/java/org/cryptomator/common/Environment.java b/src/main/java/org/cryptomator/common/Environment.java index 0fa12ee20..3cd691fae 100644 --- a/src/main/java/org/cryptomator/common/Environment.java +++ b/src/main/java/org/cryptomator/common/Environment.java @@ -33,7 +33,7 @@ public class Environment { LOG.debug("user.region: {}", System.getProperty("user.region")); LOG.debug("logback.configurationFile: {}", System.getProperty("logback.configurationFile")); LOG.debug("cryptomator.settingsPath: {}", System.getProperty("cryptomator.settingsPath")); - LOG.debug("cryptomator.ipcPortPath: {}", System.getProperty("cryptomator.ipcPortPath")); + LOG.debug("cryptomator.ipcSocketPath: {}", System.getProperty("cryptomator.ipcSocketPath")); LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath")); LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir")); LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir")); @@ -51,8 +51,8 @@ public class Environment { return getPaths("cryptomator.settingsPath"); } - public Stream getIpcPortPath() { - return getPaths("cryptomator.ipcPortPath"); + public Stream ipcSocketPath() { + return getPaths("cryptomator.ipcSocketPath"); } public Stream getKeychainPath() { @@ -99,12 +99,12 @@ public class Environment { } // visible for testing - Path getHomeDir() { + public Path getHomeDir() { return getPath("user.home").orElseThrow(); } // visible for testing - Stream getPaths(String propertyName) { + public Stream getPaths(String propertyName) { Stream rawSettingsPaths = getRawList(propertyName, PATH_LIST_SEP); return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Paths::get).map(this::replaceHomeDir); } @@ -123,8 +123,8 @@ public class Environment { return Stream.empty(); } else { Iterable iter = Splitter.on(separator).split(value); - Spliterator spliter = Spliterators.spliteratorUnknownSize(iter.iterator(), Spliterator.ORDERED | Spliterator.IMMUTABLE); - return StreamSupport.stream(spliter, false); + Spliterator spliterator = Spliterators.spliteratorUnknownSize(iter.iterator(), Spliterator.ORDERED | Spliterator.IMMUTABLE); + return StreamSupport.stream(spliterator, false); } } } diff --git a/src/main/java/org/cryptomator/common/Nullable.java b/src/main/java/org/cryptomator/common/Nullable.java new file mode 100644 index 000000000..cb0d9e887 --- /dev/null +++ b/src/main/java/org/cryptomator/common/Nullable.java @@ -0,0 +1,8 @@ +package org.cryptomator.common; + +/** + * Replacement for JSR-305 to avoid runtime dependencies. Used in Dagger components. + */ +public @interface Nullable { + +} diff --git a/src/main/java/org/cryptomator/common/SemVerComparator.java b/src/main/java/org/cryptomator/common/SemVerComparator.java index e0ff9ae25..0f9148bd5 100644 --- a/src/main/java/org/cryptomator/common/SemVerComparator.java +++ b/src/main/java/org/cryptomator/common/SemVerComparator.java @@ -60,17 +60,17 @@ public class SemVerComparator implements Comparator { final int commonCompCount = Math.min(vComps1.length, vComps2.length); for (int i = 0; i < commonCompCount; i++) { - int subversionComparisionResult = 0; + int subversionComparisonResult = 0; try { final int v1 = Integer.parseInt(vComps1[i]); final int v2 = Integer.parseInt(vComps2[i]); - subversionComparisionResult = v1 - v2; + subversionComparisonResult = v1 - v2; } catch (NumberFormatException ex) { // ok, lets compare this fragment lexicographically - subversionComparisionResult = vComps1[i].compareTo(vComps2[i]); + subversionComparisonResult = vComps1[i].compareTo(vComps2[i]); } - if (subversionComparisionResult != 0) { - return subversionComparisionResult; + if (subversionComparisonResult != 0) { + return subversionComparisonResult; } } diff --git a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java index 64db08018..57af6c685 100644 --- a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java +++ b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java @@ -59,8 +59,10 @@ public class KeychainManager implements KeychainAccessProvider { @Override public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { - getKeychainOrFail().changePassphrase(key, passphrase); - setPassphraseStored(key, true); + if (isPassphraseStored(key)) { + getKeychainOrFail().changePassphrase(key, passphrase); + setPassphraseStored(key, true); + } } @Override diff --git a/src/main/java/org/cryptomator/common/keychain/KeychainModule.java b/src/main/java/org/cryptomator/common/keychain/KeychainModule.java index 9ac343d36..01a221ed7 100644 --- a/src/main/java/org/cryptomator/common/keychain/KeychainModule.java +++ b/src/main/java/org/cryptomator/common/keychain/KeychainModule.java @@ -34,11 +34,11 @@ public class KeychainModule { @Singleton static ObjectExpression provideKeychainAccessProvider(Settings settings, Set providers) { return Bindings.createObjectBinding(() -> { - var selectedProviderClass = settings.keychainBackend().get().getProviderClass(); + var selectedProviderClass = settings.keychainProvider().get(); var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny(); var fallbackProvider = providers.stream().findAny().orElse(null); return selectedProvider.orElse(fallbackProvider); - }, settings.keychainBackend()); + }, settings.keychainProvider()); } } diff --git a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java index 3a4d82cf3..1e36ba5f6 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java @@ -1,6 +1,6 @@ package org.cryptomator.common.mountpoint; -import com.google.common.base.Preconditions; +import dagger.multibindings.IntKey; import org.cryptomator.common.vaults.Volume; import java.nio.file.Path; @@ -12,13 +12,13 @@ import java.util.SortedSet; * preparation of a mountpoint or an exception otherwise.
*

All MountPointChoosers (MPCs) need to implement this class and must be added to * the pool of possible MPCs by the {@link MountPointChooserModule MountPointChooserModule.} - * The MountPointChooserModule will sort them according to their {@link #getPriority() priority.} + * The 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.
* A specific priority must not be assigned to more than one MPC at a time; * the result of having two MPCs with equal priority is undefined. * - *

MPCs are executed by a {@link Volume} in ascending order of their priority - * (smaller priorities are tried first) to find and prepare a suitable mountpoint for the volume. + *

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 diff --git a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java b/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java index cfb7b68ec..9c7893e42 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java +++ b/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java @@ -9,6 +9,7 @@ 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; @@ -24,16 +25,22 @@ public abstract class MountPointChooserModule { @Binds @IntoMap - @IntKey(0) + @IntKey(1000) @PerVault public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser); @Binds @IntoMap - @IntKey(100) + @IntKey(900) @PerVault public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser); + @Binds + @IntoMap + @IntKey(800) + @PerVault + public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser); + @Binds @IntoMap @IntKey(101) @@ -42,13 +49,7 @@ public abstract class MountPointChooserModule { @Binds @IntoMap - @IntKey(200) - @PerVault - public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser); - - @Binds - @IntoMap - @IntKey(999) + @IntKey(100) @PerVault public abstract MountPointChooser bindTemporaryMountPointChooser(TemporaryMountPointChooser chooser); @@ -56,7 +57,8 @@ public abstract class MountPointChooserModule { @PerVault @Named("orderedMountPointChoosers") public static Iterable provideOrderedMountPointChoosers(Map choosers) { - SortedMap sortedChoosers = new TreeMap<>(choosers); + SortedMap sortedChoosers = new TreeMap<>(Comparator.reverseOrder()); + sortedChoosers.putAll(choosers); return Iterables.unmodifiableIterable(sortedChoosers.values()); } } diff --git a/src/main/java/org/cryptomator/common/settings/KeychainBackend.java b/src/main/java/org/cryptomator/common/settings/KeychainBackend.java deleted file mode 100644 index 65f869a12..000000000 --- a/src/main/java/org/cryptomator/common/settings/KeychainBackend.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.cryptomator.common.settings; - -public enum KeychainBackend { - GNOME("org.cryptomator.linux.keychain.SecretServiceKeychainAccess"), - KDE("org.cryptomator.linux.keychain.KDEWalletKeychainAccess"), - MAC_SYSTEM_KEYCHAIN("org.cryptomator.macos.keychain.MacSystemKeychainAccess"), - WIN_SYSTEM_KEYCHAIN("org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess"); - - private final String providerClass; - - KeychainBackend(String providerClass) { - this.providerClass = providerClass; - } - - public String getProviderClass() { - return providerClass; - } - -} diff --git a/src/main/java/org/cryptomator/common/settings/Settings.java b/src/main/java/org/cryptomator/common/settings/Settings.java index a419d206a..e4cb9b8f7 100644 --- a/src/main/java/org/cryptomator/common/settings/Settings.java +++ b/src/main/java/org/cryptomator/common/settings/Settings.java @@ -30,7 +30,7 @@ public class Settings { public static final int MIN_PORT = 1024; public static final int MAX_PORT = 65535; public static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false; - public static final boolean DEFAULT_CHECK_FOR_UDPATES = false; + public static final boolean DEFAULT_CHECK_FOR_UPDATES = false; public static final boolean DEFAULT_START_HIDDEN = false; public static final int DEFAULT_PORT = 42427; public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3; @@ -38,14 +38,15 @@ public class Settings { public static final boolean DEFAULT_DEBUG_MODE = false; public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = SystemUtils.IS_OS_WINDOWS ? VolumeImpl.DOKANY : VolumeImpl.FUSE; public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT; - public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = SystemUtils.IS_OS_WINDOWS ? KeychainBackend.WIN_SYSTEM_KEYCHAIN : SystemUtils.IS_OS_MAC ? KeychainBackend.MAC_SYSTEM_KEYCHAIN : KeychainBackend.GNOME; + @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 NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT; public static final String DEFAULT_LICENSE_KEY = ""; public static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false; private final ObservableList directories = FXCollections.observableArrayList(VaultSettings::observables); private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK); - private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UDPATES); + private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UPDATES); private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN); private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT); private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS); @@ -53,7 +54,7 @@ public class Settings { private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE); private final ObjectProperty preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL); private final ObjectProperty theme = new SimpleObjectProperty<>(DEFAULT_THEME); - private final ObjectProperty keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND); + private final ObjectProperty keychainProvider = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_PROVIDER); private final ObjectProperty userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION); private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY); private final BooleanProperty showMinimizeButton = new SimpleBooleanProperty(DEFAULT_SHOW_MINIMIZE_BUTTON); @@ -77,7 +78,7 @@ public class Settings { debugMode.addListener(this::somethingChanged); preferredVolumeImpl.addListener(this::somethingChanged); theme.addListener(this::somethingChanged); - keychainBackend.addListener(this::somethingChanged); + keychainProvider.addListener(this::somethingChanged); userInterfaceOrientation.addListener(this::somethingChanged); licenseKey.addListener(this::somethingChanged); showMinimizeButton.addListener(this::somethingChanged); @@ -140,7 +141,7 @@ public class Settings { return theme; } - public ObjectProperty keychainBackend() { return keychainBackend; } + public ObjectProperty keychainProvider() { return keychainProvider; } public ObjectProperty userInterfaceOrientation() { return userInterfaceOrientation; diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java index d22e0867f..5bcb5f3d7 100644 --- a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java +++ b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java @@ -48,7 +48,7 @@ public class SettingsJsonAdapter extends TypeAdapter { out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name()); out.name("theme").value(value.theme().get().name()); out.name("uiOrientation").value(value.userInterfaceOrientation().get().name()); - out.name("keychainBackend").value(value.keychainBackend().get().name()); + out.name("keychainProvider").value(value.keychainProvider().get()); out.name("licenseKey").value(value.licenseKey().get()); out.name("showMinimizeButton").value(value.showMinimizeButton().get()); out.name("showTrayIcon").value(value.showTrayIcon().get()); @@ -82,7 +82,7 @@ public class SettingsJsonAdapter extends TypeAdapter { case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString())); case "theme" -> settings.theme().set(parseUiTheme(in.nextString())); case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString())); - case "keychainBackend" -> settings.keychainBackend().set(parseKeychainBackend(in.nextString())); + case "keychainProvider" -> settings.keychainProvider().set(in.nextString()); case "licenseKey" -> settings.licenseKey().set(in.nextString()); case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean()); case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean()); @@ -124,15 +124,6 @@ public class SettingsJsonAdapter extends TypeAdapter { } } - private KeychainBackend parseKeychainBackend(String backendName) { - try { - return KeychainBackend.valueOf(backendName.toUpperCase()); - } catch (IllegalArgumentException e) { - LOG.warn("Invalid keychain backend {}. Defaulting to {}.", backendName, Settings.DEFAULT_KEYCHAIN_BACKEND); - return Settings.DEFAULT_KEYCHAIN_BACKEND; - } - } - private NodeOrientation parseUiOrientation(String uiOrientationName) { try { return NodeOrientation.valueOf(uiOrientationName.toUpperCase()); diff --git a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java b/src/main/java/org/cryptomator/common/settings/SettingsProvider.java index 8310d0fa9..3be42f7ac 100644 --- a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java +++ b/src/main/java/org/cryptomator/common/settings/SettingsProvider.java @@ -101,7 +101,7 @@ public class SettingsProvider implements Supplier { if (settings == null) { return; } - final Optional settingsPath = env.getSettingsPath().findFirst(); // alway save to preferred (first) path + final Optional settingsPath = env.getSettingsPath().findFirst(); // always save to preferred (first) path settingsPath.ifPresent(path -> { Runnable saveCommand = () -> this.save(settings, path); ScheduledFuture scheduledTask = scheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS); diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 10a023806..8ae20406c 100644 --- a/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -31,7 +31,7 @@ import java.util.Random; public class VaultSettings { public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false; - public static final boolean DEFAULT_REAVEAL_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 String DEFAULT_MOUNT_FLAGS = ""; @@ -43,11 +43,11 @@ public class VaultSettings { private static final Random RNG = new Random(); private final String id; - private final ObjectProperty path = new SimpleObjectProperty(); + private final ObjectProperty path = new SimpleObjectProperty<>(); 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 revealAfterMount = new SimpleBooleanProperty(DEFAULT_REAVEAL_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); diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java index 0cf92a48b..15a081f0d 100644 --- a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java +++ b/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java @@ -44,7 +44,7 @@ class VaultSettingsJsonAdapter { String customMountPath = null; String winDriveLetter = null; boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP; - boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT; + boolean revealAfterMount = VaultSettings.DEFAULT_REVEAL_AFTER_MOUNT; boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH; boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE; String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS; diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 02fd03600..74ac7dc40 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -19,6 +19,7 @@ import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags; import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig; +import org.cryptomator.cryptofs.VaultConfigLoadException; import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.MasterkeyLoader; @@ -327,6 +328,14 @@ public class Vault { return stats; } + /** + * Attempts to read the vault config file and parse it without verifying its integrity. + * + * @return an unverified vault config + * @throws VaultConfigLoadException if the read file cannot be properly parsed + * @throws IOException if reading the file fails + * + */ public UnverifiedVaultConfig getUnverifiedVaultConfig() throws IOException { Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME); String token = Files.readString(configPath, StandardCharsets.US_ASCII); diff --git a/src/main/java/org/cryptomator/common/vaults/VaultComponent.java b/src/main/java/org/cryptomator/common/vaults/VaultComponent.java index 47be62520..588ff64cd 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultComponent.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultComponent.java @@ -7,10 +7,10 @@ package org.cryptomator.common.vaults; import dagger.BindsInstance; import dagger.Subcomponent; +import org.cryptomator.common.Nullable; import org.cryptomator.common.mountpoint.MountPointChooserModule; import org.cryptomator.common.settings.VaultSettings; -import javax.annotation.Nullable; import javax.inject.Named; @PerVault diff --git a/src/main/java/org/cryptomator/common/vaults/VaultModule.java b/src/main/java/org/cryptomator/common/vaults/VaultModule.java index 901ee7f42..cc38e6933 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultModule.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultModule.java @@ -8,6 +8,7 @@ package org.cryptomator.common.vaults; import dagger.Module; import dagger.Provides; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.Nullable; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.settings.VolumeImpl; @@ -15,7 +16,6 @@ import org.cryptomator.cryptofs.CryptoFileSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nullable; import javax.inject.Named; import javafx.beans.binding.Bindings; import javafx.beans.binding.StringBinding; @@ -138,7 +138,7 @@ public class VaultModule { // see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse_main.c#L53-L62 for syntax guide // see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse.c#L295-L319 for options (-o <...>) - // see https://github.com/billziss-gh/winfsp/wiki/Frequently-Asked-Questions/5ba00e4be4f5e938eaae6ef1500b331de12dee77 (FUSE 4.) on why the given defaults were choosen + // 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(); diff --git a/src/main/java/org/cryptomator/common/vaults/VaultState.java b/src/main/java/org/cryptomator/common/vaults/VaultState.java index 801ea7653..51365fbd2 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultState.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultState.java @@ -46,7 +46,7 @@ public class VaultState extends ObservableValueBase implements UNLOCKED, /** - * Unknown state due to preceeding unrecoverable exceptions. + * Unknown state due to preceding unrecoverable exceptions. */ ERROR; } diff --git a/src/main/java/org/cryptomator/common/vaults/VaultStats.java b/src/main/java/org/cryptomator/common/vaults/VaultStats.java index 649be3a09..ac0b8df38 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultStats.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultStats.java @@ -35,8 +35,8 @@ public class VaultStats { private final LongProperty bytesPerSecondEncrypted = new SimpleLongProperty(); private final LongProperty bytesPerSecondDecrypted = new SimpleLongProperty(); private final DoubleProperty cacheHitRate = new SimpleDoubleProperty(); - private final LongProperty toalBytesRead = new SimpleLongProperty(); - private final LongProperty toalBytesWritten = new SimpleLongProperty(); + private final LongProperty totalBytesRead = new SimpleLongProperty(); + private final LongProperty totalBytesWritten = new SimpleLongProperty(); private final LongProperty totalBytesEncrypted = new SimpleLongProperty(); private final LongProperty totalBytesDecrypted = new SimpleLongProperty(); private final LongProperty filesRead = new SimpleLongProperty(); @@ -75,8 +75,8 @@ public class VaultStats { cacheHitRate.set(stats.map(this::getCacheHitRate).orElse(0.0)); bytesPerSecondDecrypted.set(stats.map(CryptoFileSystemStats::pollBytesDecrypted).orElse(0L)); bytesPerSecondEncrypted.set(stats.map(CryptoFileSystemStats::pollBytesEncrypted).orElse(0L)); - toalBytesRead.set(stats.map(CryptoFileSystemStats::pollTotalBytesRead).orElse(0L)); - toalBytesWritten.set(stats.map(CryptoFileSystemStats::pollTotalBytesWritten).orElse(0L)); + totalBytesRead.set(stats.map(CryptoFileSystemStats::pollTotalBytesRead).orElse(0L)); + totalBytesWritten.set(stats.map(CryptoFileSystemStats::pollTotalBytesWritten).orElse(0L)); totalBytesEncrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesEncrypted).orElse(0L)); totalBytesDecrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesDecrypted).orElse(0L)); var oldAccessCount = filesRead.get() + filesWritten.get(); @@ -146,7 +146,7 @@ public class VaultStats { return bytesPerSecondEncrypted; } - public long getBytesPerSecondEnrypted() { + public long getBytesPerSecondEncrypted() { return bytesPerSecondEncrypted.get(); } @@ -164,13 +164,13 @@ public class VaultStats { return cacheHitRate.get(); } - public LongProperty toalBytesReadProperty() {return toalBytesRead;} + public LongProperty totalBytesReadProperty() {return totalBytesRead;} - public long getTotalBytesRead() { return toalBytesRead.get();} + public long getTotalBytesRead() { return totalBytesRead.get();} - public LongProperty toalBytesWrittenProperty() {return toalBytesWritten;} + public LongProperty totalBytesWrittenProperty() {return totalBytesWritten;} - public long getTotalBytesWritten() { return toalBytesWritten.get();} + public long getTotalBytesWritten() { return totalBytesWritten.get();} public LongProperty totalBytesEncryptedProperty() {return totalBytesEncrypted;} diff --git a/src/main/java/org/cryptomator/common/vaults/Volume.java b/src/main/java/org/cryptomator/common/vaults/Volume.java index f608122bf..5f434fa43 100644 --- a/src/main/java/org/cryptomator/common/vaults/Volume.java +++ b/src/main/java/org/cryptomator/common/vaults/Volume.java @@ -12,7 +12,7 @@ import java.util.function.Consumer; import java.util.stream.Stream; /** - * Takes a Volume and usess it to mount an unlocked vault + * Takes a Volume and uses it to mount an unlocked vault */ public interface Volume { @@ -24,7 +24,7 @@ public interface Volume { boolean isSupported(); /** - * Gets the coresponding enum type of the {@link VolumeImpl volume implementation ("VolumeImpl")} that is implemented by this Volume. + * 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} */ diff --git a/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java b/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java index 03c83377c..b1850bc1e 100644 --- a/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java +++ b/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java @@ -67,7 +67,7 @@ public class WebDavVolume implements Volume { 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 specifc one or there is no free. + //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 driveLetterSupplier; if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) { driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null); diff --git a/src/main/java/org/cryptomator/ipc/Client.java b/src/main/java/org/cryptomator/ipc/Client.java new file mode 100644 index 000000000..fcd084032 --- /dev/null +++ b/src/main/java/org/cryptomator/ipc/Client.java @@ -0,0 +1,65 @@ +package org.cryptomator.ipc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.SocketChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.concurrent.Executor; + +class Client implements IpcCommunicator { + + private static final Logger LOG = LoggerFactory.getLogger(Client.class); + + private final SocketChannel socketChannel; + + private Client(SocketChannel socketChannel) { + this.socketChannel = socketChannel; + } + + public static Client create(Path socketPath) throws IOException { + var address = UnixDomainSocketAddress.of(socketPath); + var socketChannel = SocketChannel.open(address); + LOG.info("Connected to IPC server on socket {}", socketPath); + return new Client(socketChannel); + } + + @Override + public boolean isClient() { + return true; + } + + @Override + public void listen(IpcMessageListener listener, Executor executor) { + executor.execute(() -> { + try { + while (socketChannel.isConnected()) { + var msg = IpcMessage.receive(socketChannel); + listener.handleMessage(msg); + } + } catch (IOException e) { + LOG.error("Failed to read IPC message", e); + } + }); + } + + @Override + public void send(IpcMessage message, Executor executor) { + executor.execute(() -> { + try { + message.send(socketChannel); + } catch (IOException e) { + LOG.error("Failed to send IPC message", e); + } + }); + } + + @Override + public void close() throws IOException { + socketChannel.close(); + } +} diff --git a/src/main/java/org/cryptomator/ipc/HandleLaunchArgsMessage.java b/src/main/java/org/cryptomator/ipc/HandleLaunchArgsMessage.java new file mode 100644 index 000000000..0a7916c16 --- /dev/null +++ b/src/main/java/org/cryptomator/ipc/HandleLaunchArgsMessage.java @@ -0,0 +1,30 @@ +package org.cryptomator.ipc; + +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; + +record HandleLaunchArgsMessage(List args) implements IpcMessage { + + private static final char DELIMITER = '\n'; + + public static HandleLaunchArgsMessage decode(ByteBuffer encoded) { + var str = StandardCharsets.UTF_8.decode(encoded).toString(); + var args = Splitter.on(DELIMITER).omitEmptyStrings().splitToList(str); + return new HandleLaunchArgsMessage(args); + } + + @Override + public MessageType getMessageType() { + return MessageType.HANDLE_LAUNCH_ARGS; + } + + @Override + public ByteBuffer encodePayload() { + var str = Joiner.on(DELIMITER).join(args); + return StandardCharsets.UTF_8.encode(str); + } +} diff --git a/src/main/java/org/cryptomator/ipc/IpcCommunicator.java b/src/main/java/org/cryptomator/ipc/IpcCommunicator.java new file mode 100644 index 000000000..0120389c9 --- /dev/null +++ b/src/main/java/org/cryptomator/ipc/IpcCommunicator.java @@ -0,0 +1,96 @@ +package org.cryptomator.ipc; + +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.MoreExecutors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; + +public interface IpcCommunicator extends Closeable { + + Logger LOG = LoggerFactory.getLogger(IpcCommunicator.class); + + /** + * Attempts to establish a socket connection via one of the given paths. + *

+ * If no connection to an existing sockets can be established, a new socket is created for the first given path. + *

+ * If this fails as well, a fallback communicator is returned that allows process-internal communication mocking the API + * that would have been used for IPC. + * + * @param socketPaths The socket path(s) + * @return A communicator object that allows sending and receiving messages + */ + static IpcCommunicator create(Iterable socketPaths) { + Preconditions.checkArgument(socketPaths.iterator().hasNext(), "socketPaths must contain at least one element"); + for (var p : socketPaths) { + try { + var attr = Files.readAttributes(p, BasicFileAttributes.class); + if (attr.isOther()) { + return Client.create(p); + } + } catch (IOException e) { + // attempt next socket path + } + } + // Didn't get any connection yet? I.e. we're the first app instance, so let's launch a server: + try { + return Server.create(socketPaths.iterator().next()); + } catch (IOException e) { + LOG.warn("Failed to create IPC server", e); + return new LoopbackCommunicator(); + } + } + + boolean isClient(); + + /** + * Listens to incoming messages until the connection gets closed. + * @param listener The listener that should be notified of incoming messages + * @param executor An executor on which to listen. Listening will block, so you might want to use a background thread. + * @return + */ + void listen(IpcMessageListener listener, Executor executor); + + /** + * Sends the given message. + * + * @param message The message to send + * @param executor An executor used to send the message. Sending will block, so you might want to use a background thread. + */ + void send(IpcMessage message, Executor executor); + + default void sendRevealRunningApp() { + send(new RevealRunningAppMessage(), MoreExecutors.directExecutor()); + } + + default void sendHandleLaunchargs(List args) { + send(new HandleLaunchArgsMessage(args), MoreExecutors.directExecutor()); + } + + /** + * Clean up resources. + * + * @implSpec Must be idempotent + * @throws IOException In case of I/O errors. + */ + @Override + void close() throws IOException; + + default void closeUnchecked() throws UncheckedIOException { + try { + close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/main/java/org/cryptomator/ipc/IpcMessage.java b/src/main/java/org/cryptomator/ipc/IpcMessage.java new file mode 100644 index 000000000..9d1c8d3de --- /dev/null +++ b/src/main/java/org/cryptomator/ipc/IpcMessage.java @@ -0,0 +1,68 @@ +package org.cryptomator.ipc; + +import org.cryptomator.cryptolib.common.ByteBuffers; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.function.Function; + +// TODO make sealed, remove enum +interface IpcMessage { + + enum MessageType { + REVEAL_RUNNING_APP(RevealRunningAppMessage::decode), + HANDLE_LAUNCH_ARGS(HandleLaunchArgsMessage::decode); + + private final Function decoder; + + MessageType(Function decoder) { + this.decoder = decoder; + } + + static MessageType forOrdinal(int ordinal) { + try { + return values()[ordinal]; + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException("No such message type: " + ordinal, e); + } + } + + IpcMessage decodePayload(ByteBuffer payload) { + return decoder.apply(payload); + } + } + + MessageType getMessageType(); + + ByteBuffer encodePayload(); + + static IpcMessage receive(ReadableByteChannel channel) throws IOException { + var header = ByteBuffer.allocate(2 * Integer.BYTES); + if (ByteBuffers.fill(channel, header) < header.capacity()) { + throw new EOFException(); + } + header.flip(); + int typeNo = header.getInt(); + int length = header.getInt(); + MessageType type = MessageType.forOrdinal(typeNo); + var payload = ByteBuffer.allocate(length); + ByteBuffers.fill(channel, payload); + payload.flip(); + return type.decodePayload(payload); + } + + default void send(WritableByteChannel channel) throws IOException { + var payload = encodePayload(); + var buf = ByteBuffer.allocate(2 * Integer.BYTES + payload.remaining()); + buf.putInt(getMessageType().ordinal()); // message type + buf.putInt(payload.remaining()); // message length + buf.put(payload); // message + buf.flip(); + while (buf.hasRemaining()) { + channel.write(buf); + } + } +} diff --git a/src/main/java/org/cryptomator/ipc/IpcMessageListener.java b/src/main/java/org/cryptomator/ipc/IpcMessageListener.java new file mode 100644 index 000000000..f49275824 --- /dev/null +++ b/src/main/java/org/cryptomator/ipc/IpcMessageListener.java @@ -0,0 +1,19 @@ +package org.cryptomator.ipc; + +import java.util.List; + +public interface IpcMessageListener { + + default void handleMessage(IpcMessage message) { + if (message instanceof RevealRunningAppMessage) { + revealRunningApp(); + } else if (message instanceof HandleLaunchArgsMessage m) { + handleLaunchArgs(m.args()); + } + } + + void revealRunningApp(); + + void handleLaunchArgs(List args); + +} diff --git a/src/main/java/org/cryptomator/ipc/LoopbackCommunicator.java b/src/main/java/org/cryptomator/ipc/LoopbackCommunicator.java new file mode 100644 index 000000000..ba5152c93 --- /dev/null +++ b/src/main/java/org/cryptomator/ipc/LoopbackCommunicator.java @@ -0,0 +1,50 @@ +package org.cryptomator.ipc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TransferQueue; + +class LoopbackCommunicator implements IpcCommunicator { + + private static final Logger LOG = LoggerFactory.getLogger(LoopbackCommunicator.class); + + private final TransferQueue transferQueue = new LinkedTransferQueue<>(); + + @Override + public boolean isClient() { + return false; + } + + @Override + public void listen(IpcMessageListener listener, Executor executor) { + executor.execute(() -> { + try { + var msg = transferQueue.take(); + listener.handleMessage(msg); + } catch (InterruptedException e) { + LOG.error("Failed to read IPC message", e); + Thread.currentThread().interrupt(); + } + }); + } + + @Override + public void send(IpcMessage message, Executor executor) { + executor.execute(() -> { + try { + transferQueue.put(message); + } catch (InterruptedException e) { + LOG.error("Failed to send IPC message", e); + Thread.currentThread().interrupt(); + } + }); + } + + @Override + public void close() { + // no-op + } +} diff --git a/src/main/java/org/cryptomator/ipc/RevealRunningAppMessage.java b/src/main/java/org/cryptomator/ipc/RevealRunningAppMessage.java new file mode 100644 index 000000000..fa4d1375b --- /dev/null +++ b/src/main/java/org/cryptomator/ipc/RevealRunningAppMessage.java @@ -0,0 +1,20 @@ +package org.cryptomator.ipc; + +import java.nio.ByteBuffer; + +public record RevealRunningAppMessage() implements IpcMessage { + + static RevealRunningAppMessage decode(ByteBuffer ignored) { + return new RevealRunningAppMessage(); + } + + @Override + public MessageType getMessageType() { + return MessageType.REVEAL_RUNNING_APP; + } + + @Override + public ByteBuffer encodePayload() { + return ByteBuffer.allocate(0); + } +} diff --git a/src/main/java/org/cryptomator/ipc/Server.java b/src/main/java/org/cryptomator/ipc/Server.java new file mode 100644 index 000000000..e9a82c328 --- /dev/null +++ b/src/main/java/org/cryptomator/ipc/Server.java @@ -0,0 +1,82 @@ +package org.cryptomator.ipc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.EOFException; +import java.io.IOException; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.AsynchronousCloseException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ServerSocketChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.Executor; + +class Server implements IpcCommunicator { + + private static final Logger LOG = LoggerFactory.getLogger(Server.class); + + private final ServerSocketChannel serverSocketChannel; + private final Path socketPath; + + private Server(ServerSocketChannel serverSocketChannel, Path socketPath) { + this.serverSocketChannel = serverSocketChannel; + this.socketPath = socketPath; + } + + public static Server create(Path socketPath) throws IOException { + var address = UnixDomainSocketAddress.of(socketPath); + var serverSocketChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX); + serverSocketChannel.bind(address); + LOG.info("Spawning IPC server listening on socket {}", socketPath); + return new Server(serverSocketChannel, socketPath); + } + + @Override + public boolean isClient() { + return false; + } + + @Override + public void listen(IpcMessageListener listener, Executor executor) { + executor.execute(() -> { + while (serverSocketChannel.isOpen()) { + try (var ch = serverSocketChannel.accept()) { + while (ch.isConnected()) { + var msg = IpcMessage.receive(ch); + listener.handleMessage(msg); + } + } catch (AsynchronousCloseException e) { + return; // serverSocketChannel closed or listener interrupted + } catch (EOFException | ClosedChannelException e) { + // continue with next connected client + } catch (IOException e) { + LOG.error("Failed to read IPC message", e); + } + } + }); + } + + @Override + public void send(IpcMessage message, Executor executor) { + executor.execute(() -> { + try (var ch = serverSocketChannel.accept()) { + message.send(ch); + } catch (IOException e) { + LOG.error("Failed to send IPC message", e); + } + }); + } + + @Override + public void close() throws IOException { + try { + serverSocketChannel.close(); + } finally { + Files.deleteIfExists(socketPath); + LOG.debug("IPC server closed"); + } + } +} diff --git a/src/main/java/org/cryptomator/launcher/Cryptomator.java b/src/main/java/org/cryptomator/launcher/Cryptomator.java index 04eb9448d..18a748fd8 100644 --- a/src/main/java/org/cryptomator/launcher/Cryptomator.java +++ b/src/main/java/org/cryptomator/launcher/Cryptomator.java @@ -5,7 +5,12 @@ *******************************************************************************/ package org.cryptomator.launcher; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import dagger.Lazy; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.Environment; +import org.cryptomator.common.ShutdownHook; +import org.cryptomator.ipc.IpcCommunicator; import org.cryptomator.logging.DebugMode; import org.cryptomator.logging.LoggerConfiguration; import org.cryptomator.ui.launcher.UiLauncher; @@ -16,8 +21,10 @@ import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import java.io.IOException; +import java.util.List; import java.util.Optional; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; @Singleton public class Cryptomator { @@ -29,23 +36,28 @@ public class Cryptomator { private final LoggerConfiguration logConfig; private final DebugMode debugMode; - private final IpcFactory ipcFactory; + private final Environment env; + private final Lazy ipcMessageHandler; private final Optional applicationVersion; private final CountDownLatch shutdownLatch; - private final UiLauncher uiLauncher; + private final ShutdownHook shutdownHook; + private final Lazy uiLauncher; @Inject - Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, UiLauncher uiLauncher) { + Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, Environment env, Lazy ipcMessageHandler, @Named("applicationVersion") Optional applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, Lazy uiLauncher) { this.logConfig = logConfig; this.debugMode = debugMode; - this.ipcFactory = ipcFactory; + this.env = env; + this.ipcMessageHandler = ipcMessageHandler; this.applicationVersion = applicationVersion; this.shutdownLatch = shutdownLatch; + this.shutdownHook = shutdownHook; this.uiLauncher = uiLauncher; } public static void main(String[] args) { int exitCode = CRYPTOMATOR_COMPONENT.application().run(args); + LOG.info("Exit {}", exitCode); System.exit(exitCode); // end remaining non-daemon threads. } @@ -64,19 +76,24 @@ public class Cryptomator { * Attempts to create an IPC connection to a running Cryptomator instance and sends it the given args. * If no external process could be reached, the args will be handled by the loopback IPC endpoint. */ - try (IpcFactory.IpcEndpoint endpoint = ipcFactory.create()) { - endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self. - if (endpoint.isConnectedToRemote()) { - endpoint.getRemote().revealRunningApp(); + try (var communicator = IpcCommunicator.create(env.ipcSocketPath().toList())) { + if (communicator.isClient()) { + communicator.sendHandleLaunchargs(List.of(args)); + communicator.sendRevealRunningApp(); LOG.info("Found running application instance. Shutting down..."); return 2; } else { + shutdownHook.runOnShutdown(communicator::closeUnchecked); + var executor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("IPC-%d").build()); + var msgHandler = ipcMessageHandler.get(); + msgHandler.handleLaunchArgs(List.of(args)); + communicator.listen(msgHandler, executor); LOG.debug("Did not find running application instance. Launching GUI..."); return runGuiApplication(); } - } catch (IOException e) { - LOG.error("Failed to initiate inter-process communication.", e); - return runGuiApplication(); + } catch (Throwable e) { + LOG.error("Running application failed", e); + return 1; } } @@ -88,7 +105,7 @@ public class Cryptomator { */ private int runGuiApplication() { try { - uiLauncher.launch(); + uiLauncher.get().launch(); shutdownLatch.await(); LOG.info("UI shut down"); return 0; @@ -98,5 +115,4 @@ public class Cryptomator { } } - } diff --git a/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java b/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java index 9d1e3144d..b4e37e1f9 100644 --- a/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java +++ b/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java @@ -22,6 +22,7 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.stream.Collectors; @@ -46,13 +47,13 @@ class FileOpenRequestHandler { tryToEnqueueFileOpenRequest(launchEvent); } - public void handleLaunchArgs(String[] args) { + public void handleLaunchArgs(List args) { handleLaunchArgs(FileSystems.getDefault(), args); } // visible for testing - void handleLaunchArgs(FileSystem fs, String[] args) { - Collection pathsToOpen = Arrays.stream(args).map(str -> { + void handleLaunchArgs(FileSystem fs, List args) { + Collection pathsToOpen = args.stream().map(str -> { try { return fs.getPath(str); } catch (InvalidPathException e) { diff --git a/src/main/java/org/cryptomator/launcher/IpcFactory.java b/src/main/java/org/cryptomator/launcher/IpcFactory.java deleted file mode 100644 index 112050d26..000000000 --- a/src/main/java/org/cryptomator/launcher/IpcFactory.java +++ /dev/null @@ -1,258 +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.launcher; - -import com.google.common.io.MoreFiles; -import org.cryptomator.common.Environment; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import javax.inject.Singleton; -import java.io.Closeable; -import java.io.IOException; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.rmi.NotBoundException; -import java.rmi.registry.LocateRegistry; -import java.rmi.registry.Registry; -import java.rmi.server.RMIClientSocketFactory; -import java.rmi.server.RMIServerSocketFactory; -import java.rmi.server.RMISocketFactory; -import java.rmi.server.UnicastRemoteObject; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -/** - * First running application on a machine opens a server socket. Further processes will connect as clients. - */ -@Singleton -class IpcFactory { - - private static final Logger LOG = LoggerFactory.getLogger(IpcFactory.class); - private static final String RMI_NAME = "Cryptomator"; - - private final List portFilePaths; - private final IpcProtocolImpl ipcHandler; - - @Inject - public IpcFactory(Environment env, IpcProtocolImpl ipcHandler) { - this.portFilePaths = env.getIpcPortPath().collect(Collectors.toUnmodifiableList()); - this.ipcHandler = ipcHandler; - } - - public IpcEndpoint create() { - if (portFilePaths.isEmpty()) { - LOG.warn("No IPC port file path specified."); - return new SelfEndpoint(ipcHandler); - } else { - System.setProperty("java.rmi.server.hostname", "localhost"); - return attemptClientConnection().or(this::createServerEndpoint).orElseGet(() -> new SelfEndpoint(ipcHandler)); - } - } - - private Optional attemptClientConnection() { - for (Path portFilePath : portFilePaths) { - try { - int port = readPort(portFilePath); - LOG.debug("[Client] Connecting to port {}...", port); - Registry registry = LocateRegistry.getRegistry("localhost", port, new ClientSocketFactory()); - IpcProtocol remoteInterface = (IpcProtocol) registry.lookup(RMI_NAME); - return Optional.of(new ClientEndpoint(remoteInterface)); - } catch (NotBoundException | IOException e) { - LOG.debug("[Client] Failed to connect."); - // continue with next portFilePath... - } - } - return Optional.empty(); - } - - private int readPort(Path portFilePath) throws IOException { - try (ReadableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.READ)) { - LOG.debug("[Client] Reading IPC port from {}", portFilePath); - ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); - if (ch.read(buf) == Integer.BYTES) { - buf.flip(); - return buf.getInt(); - } else { - throw new IOException("Invalid IPC port file."); - } - } - } - - private Optional createServerEndpoint() { - assert !portFilePaths.isEmpty(); - Path portFilePath = portFilePaths.get(0); - try { - ServerSocket socket = new ServerSocket(0, Byte.MAX_VALUE, InetAddress.getByName("localhost")); - RMIClientSocketFactory csf = RMISocketFactory.getDefaultSocketFactory(); - SingletonServerSocketFactory ssf = new SingletonServerSocketFactory(socket); - Registry registry = LocateRegistry.createRegistry(0, csf, ssf); - UnicastRemoteObject.exportObject(ipcHandler, 0); - registry.rebind(RMI_NAME, ipcHandler); - writePort(portFilePath, socket.getLocalPort()); - return Optional.of(new ServerEndpoint(ipcHandler, socket, registry, portFilePath)); - } catch (IOException e) { - LOG.warn("[Server] Failed to create IPC server.", e); - return Optional.empty(); - } - } - - private void writePort(Path portFilePath, int port) throws IOException { - ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); - buf.putInt(port); - buf.flip(); - MoreFiles.createParentDirectories(portFilePath); - try (WritableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { - if (ch.write(buf) != Integer.BYTES) { - throw new IOException("Did not write expected number of bytes."); - } - } - LOG.debug("[Server] Wrote IPC port {} to {}", port, portFilePath); - } - - interface IpcEndpoint extends Closeable { - - boolean isConnectedToRemote(); - - IpcProtocol getRemote(); - - } - - static class SelfEndpoint implements IpcEndpoint { - - protected final IpcProtocol remoteObject; - - SelfEndpoint(IpcProtocol remoteObject) { - this.remoteObject = remoteObject; - } - - @Override - public boolean isConnectedToRemote() { - return false; - } - - @Override - public IpcProtocol getRemote() { - return remoteObject; - } - - @Override - public void close() { - // no-op - } - } - - static class ClientEndpoint implements IpcEndpoint { - - private final IpcProtocol remoteInterface; - - public ClientEndpoint(IpcProtocol remoteInterface) { - this.remoteInterface = remoteInterface; - } - - public IpcProtocol getRemote() { - return remoteInterface; - } - - @Override - public boolean isConnectedToRemote() { - return true; - } - - @Override - public void close() { - // no-op - } - - } - - class ServerEndpoint extends SelfEndpoint { - - private final ServerSocket socket; - private final Registry registry; - private final Path portFilePath; - - private ServerEndpoint(IpcProtocol remoteObject, ServerSocket socket, Registry registry, Path portFilePath) { - super(remoteObject); - this.socket = socket; - this.registry = registry; - this.portFilePath = portFilePath; - } - - @Override - public void close() { - try { - registry.unbind(RMI_NAME); - UnicastRemoteObject.unexportObject(remoteObject, true); - socket.close(); - Files.deleteIfExists(portFilePath); - LOG.debug("[Server] Shut down"); - } catch (NotBoundException | IOException e) { - LOG.warn("[Server] Error shutting down:", e); - } - } - - } - - /** - * Always returns the same pre-constructed server socket. - */ - private static class SingletonServerSocketFactory implements RMIServerSocketFactory { - - private final ServerSocket socket; - - public SingletonServerSocketFactory(ServerSocket socket) { - this.socket = socket; - } - - @Override - public synchronized ServerSocket createServerSocket(int port) throws IOException { - if (port != 0) { - throw new IllegalArgumentException("This factory doesn't support specific ports."); - } - return this.socket; - } - - } - - /** - * Creates client sockets with short timeouts. - */ - private static class ClientSocketFactory implements RMIClientSocketFactory { - - @Override - public Socket createSocket(String host, int port) throws IOException { - return new SocketWithFixedTimeout(host, port, 1000); - } - - } - - private static class SocketWithFixedTimeout extends Socket { - - public SocketWithFixedTimeout(String host, int port, int timeoutInMs) throws UnknownHostException, IOException { - super(host, port); - super.setSoTimeout(timeoutInMs); - } - - @Override - public synchronized void setSoTimeout(int timeout) throws SocketException { - // do nothing, timeout is fixed - } - - } - -} diff --git a/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java b/src/main/java/org/cryptomator/launcher/IpcMessageHandler.java similarity index 60% rename from src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java rename to src/main/java/org/cryptomator/launcher/IpcMessageHandler.java index 44f67e0cd..5c28d05a4 100644 --- a/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java +++ b/src/main/java/org/cryptomator/launcher/IpcMessageHandler.java @@ -1,5 +1,6 @@ package org.cryptomator.launcher; +import org.cryptomator.ipc.IpcMessageListener; import org.cryptomator.ui.launcher.AppLaunchEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -7,20 +8,20 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; -import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.concurrent.BlockingQueue; @Singleton -class IpcProtocolImpl implements IpcProtocol { +class IpcMessageHandler implements IpcMessageListener { - private static final Logger LOG = LoggerFactory.getLogger(IpcProtocolImpl.class); + private static final Logger LOG = LoggerFactory.getLogger(IpcMessageHandler.class); private final FileOpenRequestHandler fileOpenRequestHandler; private final BlockingQueue launchEventQueue; @Inject - public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue launchEventQueue) { + public IpcMessageHandler(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue launchEventQueue) { this.fileOpenRequestHandler = fileOpenRequestHandler; this.launchEventQueue = launchEventQueue; } @@ -31,8 +32,8 @@ class IpcProtocolImpl implements IpcProtocol { } @Override - public void handleLaunchArgs(String... args) { - LOG.debug("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse("")); + public void handleLaunchArgs(List args) { + LOG.debug("Received launch args: {}", args.stream().reduce((a, b) -> a + ", " + b).orElse("")); fileOpenRequestHandler.handleLaunchArgs(args); } diff --git a/src/main/java/org/cryptomator/launcher/IpcProtocol.java b/src/main/java/org/cryptomator/launcher/IpcProtocol.java deleted file mode 100644 index 3e0596d77..000000000 --- a/src/main/java/org/cryptomator/launcher/IpcProtocol.java +++ /dev/null @@ -1,17 +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.launcher; - -import java.rmi.Remote; -import java.rmi.RemoteException; - -interface IpcProtocol extends Remote { - - void revealRunningApp() throws RemoteException; - - void handleLaunchArgs(String... args) throws RemoteException; - -} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggerinPolicy.java b/src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggeringPolicy.java similarity index 86% rename from src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggerinPolicy.java rename to src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggeringPolicy.java index 3879fbdc6..25f5239ab 100644 --- a/src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggerinPolicy.java +++ b/src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggeringPolicy.java @@ -11,12 +11,12 @@ import java.io.File; * * @param Event type the policy possibly reacts to */ -public class LaunchAndSizeBasedTriggerinPolicy extends TriggeringPolicyBase { +public class LaunchAndSizeBasedTriggeringPolicy extends TriggeringPolicyBase { LaunchBasedTriggeringPolicy launchBasedTriggeringPolicy; SizeBasedTriggeringPolicy sizeBasedTriggeringPolicy; - public LaunchAndSizeBasedTriggerinPolicy(FileSize threshold) { + public LaunchAndSizeBasedTriggeringPolicy(FileSize threshold) { this.launchBasedTriggeringPolicy = new LaunchBasedTriggeringPolicy<>(); this.sizeBasedTriggeringPolicy = new SizeBasedTriggeringPolicy<>(); sizeBasedTriggeringPolicy.setMaxFileSize(threshold); diff --git a/src/main/java/org/cryptomator/logging/LoggerModule.java b/src/main/java/org/cryptomator/logging/LoggerModule.java index 0b24e0e24..4866655e3 100644 --- a/src/main/java/org/cryptomator/logging/LoggerModule.java +++ b/src/main/java/org/cryptomator/logging/LoggerModule.java @@ -85,7 +85,7 @@ public class LoggerModule { appender.setContext(context); appender.setFile(logDir.resolve(LOGFILE_NAME).toString()); appender.setEncoder(encoder); - LaunchAndSizeBasedTriggerinPolicy triggeringPolicy = new LaunchAndSizeBasedTriggerinPolicy(FileSize.valueOf(LOG_MAX_SIZE)); + LaunchAndSizeBasedTriggeringPolicy triggeringPolicy = new LaunchAndSizeBasedTriggeringPolicy(FileSize.valueOf(LOG_MAX_SIZE)); triggeringPolicy.setContext(context); triggeringPolicy.start(); appender.setTriggeringPolicy(triggeringPolicy); diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java index 4b4e02ed2..627793d6e 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java @@ -5,8 +5,8 @@ import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProvider; -import org.cryptomator.cryptofs.VaultCipherCombo; import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.common.MasterkeyFileAccess; @@ -76,6 +76,7 @@ public class CreateNewVaultPasswordController implements FxController { private final BooleanProperty readyToCreateVault; private final ObjectBinding createVaultButtonState; + /* FXML */ public ToggleGroup recoveryKeyChoice; public Toggle showRecoveryKey; public Toggle skipRecoveryKey; @@ -106,7 +107,7 @@ public class CreateNewVaultPasswordController implements FxController { @FXML public void initialize() { - readyToCreateVault.bind(newPasswordSceneController.passwordsMatchAndSufficientProperty().and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not())); + readyToCreateVault.bind(newPasswordSceneController.goodPasswordProperty().and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not())); window.setOnHiding(event -> { newPasswordSceneController.passwordField.wipe(); newPasswordSceneController.reenterField.wipe(); @@ -182,7 +183,7 @@ public class CreateNewVaultPasswordController implements FxController { // 2. initialize vault: try { MasterkeyLoader loader = ignored -> masterkey.clone(); - CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build(); + CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).withKeyLoader(loader).build(); CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID); // 3. write vault-internal readme file: diff --git a/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java b/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java index ccc0184ac..54519f21f 100644 --- a/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java +++ b/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java @@ -62,7 +62,7 @@ public class ChangePasswordController implements FxController { public void initialize() { BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not(); BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty(); - finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordController.passwordsMatchAndSufficientProperty().not())); + finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordController.goodPasswordProperty().not())); window.setOnHiding(event -> { oldPasswordField.wipe(); newPasswordController.passwordField.wipe(); diff --git a/src/main/java/org/cryptomator/ui/common/Animations.java b/src/main/java/org/cryptomator/ui/common/Animations.java index d71309933..3c703a50d 100644 --- a/src/main/java/org/cryptomator/ui/common/Animations.java +++ b/src/main/java/org/cryptomator/ui/common/Animations.java @@ -1,11 +1,17 @@ package org.cryptomator.ui.common; +import javafx.animation.Animation; +import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; +import javafx.animation.RotateTransition; +import javafx.animation.SequentialTransition; import javafx.animation.Timeline; import javafx.beans.value.WritableValue; +import javafx.scene.Node; import javafx.stage.Window; import javafx.util.Duration; +import java.util.stream.IntStream; public class Animations { @@ -33,4 +39,19 @@ public class Animations { ); } + public static SequentialTransition createDiscrete360Rotation(Node toAnimate) { + var animation = new SequentialTransition(IntStream.range(0, 8).mapToObj(i -> Animations.createDiscrete45Rotation()).toArray(Animation[]::new)); + animation.setCycleCount(Animation.INDEFINITE); + animation.setNode(toAnimate); + return animation; + } + + private static RotateTransition createDiscrete45Rotation() { + var animation = new RotateTransition(Duration.millis(100)); + animation.setInterpolator(Interpolator.DISCRETE); + animation.setByAngle(45); + animation.setCycleCount(1); + return animation; + } + } diff --git a/src/main/java/org/cryptomator/ui/common/AutoAnimator.java b/src/main/java/org/cryptomator/ui/common/AutoAnimator.java new file mode 100644 index 000000000..b5398339c --- /dev/null +++ b/src/main/java/org/cryptomator/ui/common/AutoAnimator.java @@ -0,0 +1,84 @@ +package org.cryptomator.ui.common; + +import com.tobiasdiez.easybind.EasyBind; +import com.tobiasdiez.easybind.Subscription; + +import javafx.animation.Animation; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ObservableValue; + +/** + * Animation which starts and stops automatically based on an observable condition. + *

+ * During creation the consumer can optionally define actions to be executed everytime before the animation starts and after it stops. + */ +public class AutoAnimator { + + private final T animation; + private final ObservableValue condition; + private final Runnable beforeStart; + private final Runnable afterStop; + private final Subscription sub; + + AutoAnimator(T animation, ObservableValue condition, Runnable beforeStart, Runnable afterStop) { + this.animation = animation; + this.condition = condition; + this.beforeStart = beforeStart; + this.afterStop = afterStop; + this.sub = EasyBind.subscribe(condition, this::togglePlay); + } + + public void playFromStart() { + beforeStart.run(); + animation.playFromStart(); + } + + public void stop() { + animation.stop(); + afterStop.run(); + } + + private void togglePlay(boolean play) { + if (play) { + this.playFromStart(); + } else { + this.stop(); + } + } + + public static Builder animate(Animation animation) { + return new Builder(animation); + } + + public static class Builder { + + private Animation animation; + private ObservableValue condition = new SimpleBooleanProperty(true); + private Runnable beforeStart = () -> {}; + private Runnable afterStop = () -> {}; + + private Builder(Animation animation) { + this.animation = animation; + } + + public Builder onCondition(ObservableValue condition) { + this.condition = condition; + return this; + } + + public Builder beforeStart(Runnable beforeStart) { + this.beforeStart = beforeStart; + return this; + } + + public Builder afterStop(Runnable afterStop) { + this.afterStop = afterStop; + return this; + } + + public AutoAnimator build() { + return new AutoAnimator(animation, condition, beforeStart, afterStop); + } + + } +} diff --git a/src/main/java/org/cryptomator/ui/common/ErrorComponent.java b/src/main/java/org/cryptomator/ui/common/ErrorComponent.java index 285270b4c..92276f5bd 100644 --- a/src/main/java/org/cryptomator/ui/common/ErrorComponent.java +++ b/src/main/java/org/cryptomator/ui/common/ErrorComponent.java @@ -2,8 +2,8 @@ package org.cryptomator.ui.common; import dagger.BindsInstance; import dagger.Subcomponent; +import org.cryptomator.common.Nullable; -import javax.annotation.Nullable; import javafx.application.Platform; import javafx.scene.Scene; import javafx.stage.Stage; diff --git a/src/main/java/org/cryptomator/ui/common/ErrorController.java b/src/main/java/org/cryptomator/ui/common/ErrorController.java index c7f19a9e6..85b335b15 100644 --- a/src/main/java/org/cryptomator/ui/common/ErrorController.java +++ b/src/main/java/org/cryptomator/ui/common/ErrorController.java @@ -1,6 +1,7 @@ package org.cryptomator.ui.common; -import javax.annotation.Nullable; +import org.cryptomator.common.Nullable; + import javax.inject.Inject; import javax.inject.Named; import javafx.fxml.FXML; diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index b8d5bbff0..ea0c1ed38 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -12,6 +12,7 @@ public enum FxmlFile { ERROR("/fxml/error.fxml"), // FORGET_PASSWORD("/fxml/forget_password.fxml"), // HEALTH_START("/fxml/health_start.fxml"), // + HEALTH_START_FAIL("/fxml/health_start_fail.fxml"), // HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), // LOCK_FORCED("/fxml/lock_forced.fxml"), // LOCK_FAILED("/fxml/lock_failed.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java b/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java index cf8940cc2..c10054ef4 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java @@ -5,7 +5,6 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; import java.util.Map; import java.util.ResourceBundle; @@ -26,11 +25,9 @@ public class FxmlLoaderFactory { /** * @return A new FXMLLoader instance */ - public FXMLLoader construct() { - FXMLLoader loader = new FXMLLoader(); - loader.setControllerFactory(this::constructController); - loader.setResources(resourceBundle); - return loader; + private FXMLLoader construct(String fxmlResourceName) { + var url = getClass().getResource(fxmlResourceName); + return new FXMLLoader(url, resourceBundle, null, this::constructController); } /** @@ -41,10 +38,8 @@ public class FxmlLoaderFactory { * @throws IOException if an error occurs while loading the FXML file */ public FXMLLoader load(String fxmlResourceName) throws IOException { - FXMLLoader loader = construct(); - try (InputStream in = getClass().getResourceAsStream(fxmlResourceName)) { - loader.load(in); - } + FXMLLoader loader = construct(fxmlResourceName); + loader.load(); return loader; } @@ -67,8 +62,8 @@ public class FxmlLoaderFactory { } Parent root = loader.getRoot(); // TODO: discuss if we can remove language-specific stylesheets - // List addtionalStyleSheets = Splitter.on(',').omitEmptyStrings().splitToList(resourceBundle.getString("additionalStyleSheets")); - // addtionalStyleSheets.forEach(styleSheet -> root.getStylesheets().add("/css/" + styleSheet)); + // List additionalStyleSheets = Splitter.on(',').omitEmptyStrings().splitToList(resourceBundle.getString("additionalStyleSheets")); + // additionalStyleSheets.forEach(styleSheet -> root.getStylesheets().add("/css/" + styleSheet)); return sceneFactory.apply(root); } diff --git a/src/main/java/org/cryptomator/ui/common/NewPasswordController.java b/src/main/java/org/cryptomator/ui/common/NewPasswordController.java index 13e59f2cd..caa0962f8 100644 --- a/src/main/java/org/cryptomator/ui/common/NewPasswordController.java +++ b/src/main/java/org/cryptomator/ui/common/NewPasswordController.java @@ -4,12 +4,12 @@ import com.tobiasdiez.easybind.EasyBind; import org.cryptomator.ui.controls.FontAwesome5IconView; import org.cryptomator.ui.controls.NiceSecurePasswordField; -import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.fxml.FXML; import javafx.scene.control.Label; @@ -20,7 +20,7 @@ public class NewPasswordController implements FxController { private final ResourceBundle resourceBundle; private final PasswordStrengthUtil strengthRater; private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); - private final ReadOnlyBooleanWrapper passwordsMatchAndSufficient = new ReadOnlyBooleanWrapper(); + private final BooleanProperty goodPassword = new SimpleBooleanProperty(); public NiceSecurePasswordField passwordField; public NiceSecurePasswordField reenterField; @@ -50,11 +50,10 @@ public class NewPasswordController implements FxController { passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(passwordMatchCheckmark).otherwise(passwordMatchCross)); passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("newPassword.passwordsMatch")).otherwise(resourceBundle.getString("newPassword.passwordsDoNotMatch"))); - passwordField.textProperty().addListener(this::passwordsDidChange); - reenterField.textProperty().addListener(this::passwordsDidChange); + BooleanBinding sufficientStrength = Bindings.createBooleanBinding(this::sufficientStrength, passwordField.textProperty()); + goodPassword.bind(passwordsMatch.and(sufficientStrength)); } - private FontAwesome5IconView getIconViewForPasswordStrengthLabel() { if (passwordField.getCharacters().length() == 0) { return null; @@ -67,22 +66,24 @@ public class NewPasswordController implements FxController { } } - private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) { - if (passwordFieldsMatch() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) { - passwordsMatchAndSufficient.setValue(true); - } - } - private boolean passwordFieldsMatch() { return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0; } - public ReadOnlyBooleanProperty passwordsMatchAndSufficientProperty() { - return passwordsMatchAndSufficient.getReadOnlyProperty(); + private boolean sufficientStrength() { + return strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters()); } /* Getter/Setter */ + public ReadOnlyBooleanProperty goodPasswordProperty() { + return goodPassword; + } + + public boolean isGoodPassword() { + return goodPassword.get(); + } + public IntegerProperty passwordStrengthProperty() { return passwordStrength; } diff --git a/src/main/java/org/cryptomator/ui/common/WeakBindings.java b/src/main/java/org/cryptomator/ui/common/WeakBindings.java index e6071df1d..6efa747c9 100644 --- a/src/main/java/org/cryptomator/ui/common/WeakBindings.java +++ b/src/main/java/org/cryptomator/ui/common/WeakBindings.java @@ -77,7 +77,7 @@ public final class WeakBindings { * @param observable The observable * @return a IntegerBinding weakly referenced from the given observable */ - public static IntegerBinding bindInterger(ObservableValue observable) { + public static IntegerBinding bindInteger(ObservableValue observable) { return new IntegerBinding() { { bind(observable); diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index a273447bb..15b1718e1 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -8,6 +8,8 @@ public enum FontAwesome5Icon { ARROW_UP("\uF062"), // BAN("\uF05E"), // BUG("\uF188"), // + CARET_DOWN("\uF0D7"), // + CARET_RIGHT("\uF0Da"), // CHECK("\uF00C"), // CLOCK("\uF017"), // COG("\uF013"), // @@ -20,6 +22,7 @@ public enum FontAwesome5Icon { EXCLAMATION_TRIANGLE("\uF071"), // EYE("\uF06E"), // EYE_SLASH("\uF070"), // + FAST_FORWARD("\uF050"), // FILE("\uF15B"), // FILE_IMPORT("\uF56F"), // FOLDER_OPEN("\uF07C"), // @@ -39,7 +42,7 @@ public enum FontAwesome5Icon { REDO("\uF01E"), // SEARCH("\uF002"), // SPINNER("\uF110"), // - STOPWATCH("\uF2F2"), // + STETHOSCOPE("\uF0f1"), // SYNC("\uF021"), // TIMES("\uF00D"), // TRASH("\uF1F8"), // diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java index 4d0797eaa..4c89ca674 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java @@ -21,8 +21,8 @@ public class FontAwesome5IconView extends Text { private static final String FONT_PATH = "/css/fontawesome5-free-solid.otf"; private static final Font FONT; - private ObjectProperty glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH); - private DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE); + protected final ObjectProperty glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH); + protected final DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE); static { try { @@ -42,7 +42,7 @@ public class FontAwesome5IconView extends Text { } private void glyphChanged(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") FontAwesome5Icon oldValue, FontAwesome5Icon newValue) { - setText(newValue.unicode()); + setText(newValue == null ? null : newValue.unicode()); } private void glyphSizeChanged(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") Number oldValue, Number newValue) { diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Spinner.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Spinner.java new file mode 100644 index 000000000..eb28a90a9 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Spinner.java @@ -0,0 +1,44 @@ +package org.cryptomator.ui.controls; + +import org.cryptomator.ui.common.Animations; +import org.cryptomator.ui.common.AutoAnimator; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; + +/** + * An animated progress spinner using the {@link FontAwesome5IconView} with the spinner glyph. + *

+ * Using the default constructor, the animation is always played if the icon is visible. To animate on other conditions, use the constructor with the "spinning" property. + */ +public class FontAwesome5Spinner extends FontAwesome5IconView { + + protected final BooleanProperty spinning = new SimpleBooleanProperty(this, "spinning", true); + + private AutoAnimator animator; + + public FontAwesome5Spinner() { + setGlyph(FontAwesome5Icon.SPINNER); + var animation = Animations.createDiscrete360Rotation(this); + this.animator = AutoAnimator.animate(animation) // + .afterStop(() -> setRotate(0)) // + .onCondition(spinning.and(visibleProperty())) // + .build(); + } + + /* Getter/Setter */ + + public BooleanProperty spinningProperty() { + return spinning; + } + + public boolean isSpinning() { + return spinning.get(); + } + + public void setSpinning(boolean spinning) { + this.spinning.set(spinning); + } + +} diff --git a/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java b/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java index 928cfc40e..4a4e43fff 100644 --- a/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java +++ b/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java @@ -12,8 +12,8 @@ import javafx.scene.layout.StackPane; public class NiceSecurePasswordField extends StackPane { private static final String STYLE_CLASS = "nice-secure-password-field"; - private static final String ICONS_STLYE_CLASS = "icons"; - private static final String REVEAL_BUTTON_STLYE_CLASS = "reveal-button"; + private static final String ICONS_STYLE_CLASS = "icons"; + private static final String REVEAL_BUTTON_STYLE_CLASS = "reveal-button"; private static final int ICON_SPACING = 6; private static final double ICON_SIZE = 14.0; @@ -30,7 +30,7 @@ public class NiceSecurePasswordField extends StackPane { iconContainer.setAlignment(Pos.CENTER_RIGHT); iconContainer.setMaxWidth(Double.NEGATIVE_INFINITY); iconContainer.setPrefWidth(42); // TODO - iconContainer.getStyleClass().add(ICONS_STLYE_CLASS); + iconContainer.getStyleClass().add(ICONS_STYLE_CLASS); StackPane.setAlignment(iconContainer, Pos.CENTER_RIGHT); capsLockedIcon.setGlyph(FontAwesome5Icon.ARROW_UP); @@ -51,7 +51,7 @@ public class NiceSecurePasswordField extends StackPane { revealPasswordButton.setFocusTraversable(false); revealPasswordButton.visibleProperty().bind(passwordField.focusedProperty()); revealPasswordButton.managedProperty().bind(passwordField.focusedProperty()); - revealPasswordButton.getStyleClass().add(REVEAL_BUTTON_STLYE_CLASS); + revealPasswordButton.getStyleClass().add(REVEAL_BUTTON_STYLE_CLASS); passwordField.revealPasswordProperty().bind(revealPasswordButton.selectedProperty()); diff --git a/src/main/java/org/cryptomator/ui/controls/ThrougputLabel.java b/src/main/java/org/cryptomator/ui/controls/ThroughputLabel.java similarity index 96% rename from src/main/java/org/cryptomator/ui/controls/ThrougputLabel.java rename to src/main/java/org/cryptomator/ui/controls/ThroughputLabel.java index a21e6d916..19999ea11 100644 --- a/src/main/java/org/cryptomator/ui/controls/ThrougputLabel.java +++ b/src/main/java/org/cryptomator/ui/controls/ThroughputLabel.java @@ -8,7 +8,7 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.control.Label; -public class ThrougputLabel extends Label { +public class ThroughputLabel extends Label { private static final long KIBS_THRESHOLD = 1l << 7; // 0.128 kiB/s private static final long MIBS_THRESHOLD = 1l << 19; // 0.512 MiB/s @@ -18,7 +18,7 @@ public class ThrougputLabel extends Label { private final StringProperty mibsFormat = new SimpleStringProperty("%.3f"); private final LongProperty bytesPerSecond = new SimpleLongProperty(); - public ThrougputLabel() { + public ThroughputLabel() { textProperty().bind(createStringBinding()); } diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index 02a2e5b9e..1812d38bd 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -188,9 +188,12 @@ public class FxApplication extends Application { } private void applySystemTheme() { - appearanceProvider.ifPresent(appearanceProvider -> { - systemInterfaceThemeChanged(appearanceProvider.getSystemTheme()); - }); + if (appearanceProvider.isPresent()) { + systemInterfaceThemeChanged(appearanceProvider.get().getSystemTheme()); + } else { + LOG.warn("No UiAppearanceProvider present, assuming LIGHT theme..."); + applyLightTheme(); + } } private void applyLightTheme() { diff --git a/src/main/java/org/cryptomator/ui/health/BatchService.java b/src/main/java/org/cryptomator/ui/health/BatchService.java deleted file mode 100644 index f3968c27d..000000000 --- a/src/main/java/org/cryptomator/ui/health/BatchService.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.cryptomator.ui.health; - -import com.google.common.base.Preconditions; -import com.google.common.base.Suppliers; -import dagger.Lazy; - -import javax.inject.Inject; -import javafx.concurrent.Service; -import javafx.concurrent.Task; -import java.util.Collection; -import java.util.Iterator; -import java.util.concurrent.ExecutorService; -import java.util.function.Supplier; - -public class BatchService extends Service { - - private final Iterator remainingTasks; - - @Inject - public BatchService(Iterable tasks) { - this.remainingTasks = tasks.iterator(); - } - - @Override - protected Task createTask() { - Preconditions.checkState(remainingTasks.hasNext(), "No remaining tasks"); - return remainingTasks.next(); - } - - @Override - protected void succeeded() { - if (remainingTasks.hasNext()) { - this.restart(); - } - } -} diff --git a/src/main/java/org/cryptomator/ui/health/Check.java b/src/main/java/org/cryptomator/ui/health/Check.java new file mode 100644 index 000000000..52bee578c --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/Check.java @@ -0,0 +1,103 @@ +package org.cryptomator.ui.health; + +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.cryptofs.health.api.HealthCheck; + +import javafx.beans.Observable; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +public class Check { + + private final HealthCheck check; + + private final BooleanProperty chosenForExecution = new SimpleBooleanProperty(false); + private final ObjectProperty state = new SimpleObjectProperty<>(CheckState.RUNNABLE); + private final ObservableList results = FXCollections.observableArrayList(Result::observables); + private final ObjectProperty highestResultSeverity = new SimpleObjectProperty<>(null); + private final ObjectProperty error = new SimpleObjectProperty<>(null); + private final BooleanBinding isInReRunState = state.isNotEqualTo(CheckState.RUNNING).or(state.isNotEqualTo(CheckState.SCHEDULED)); + + Check(HealthCheck check) { + this.check = check; + } + + String getName() { + return check.name(); + } + + HealthCheck getHealthCheck() { + return check; + } + + BooleanProperty chosenForExecutionProperty() { + return chosenForExecution; + } + + boolean isChosenForExecution() { + return chosenForExecution.get(); + } + + ObjectProperty stateProperty() { + return state; + } + + CheckState getState() { + return state.get(); + } + + void setState(CheckState newState) { + state.set(newState); + } + + ObjectProperty errorProperty() { + return error; + } + + Throwable getError() { + return error.get(); + } + + void setError(Throwable t) { + error.set(t); + } + + ObjectProperty highestResultSeverityProperty() { + return highestResultSeverity; + } + + DiagnosticResult.Severity getHighestResultSeverity() { + return highestResultSeverity.get(); + } + + void setHighestResultSeverity(DiagnosticResult.Severity severity) { + highestResultSeverity.set(severity); + } + + boolean isInReRunState() { + return isInReRunState.get(); + } + + enum CheckState { + RUNNABLE, + SCHEDULED, + RUNNING, + SUCCEEDED, + SKIPPED, + ERROR, + CANCELLED; + } + + ObservableList getResults() { + return results; + } + + Observable[] observables() { + return new Observable[]{chosenForExecution, state, results, error}; + } +} diff --git a/src/main/java/org/cryptomator/ui/health/CheckDetailController.java b/src/main/java/org/cryptomator/ui/health/CheckDetailController.java index d579ff709..66f2e9bf5 100644 --- a/src/main/java/org/cryptomator/ui/health/CheckDetailController.java +++ b/src/main/java/org/cryptomator/ui/health/CheckDetailController.java @@ -12,7 +12,6 @@ import javafx.beans.binding.Binding; import javafx.beans.property.ObjectProperty; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; -import javafx.concurrent.Worker; import javafx.fxml.FXML; import javafx.scene.control.ListView; import java.util.function.Function; @@ -21,54 +20,56 @@ import java.util.stream.Stream; @HealthCheckScoped public class CheckDetailController implements FxController { - private final EasyObservableList results; - private final OptionalBinding taskState; - private final Binding taskName; - private final Binding taskDuration; - private final ResultListCellFactory resultListCellFactory; - private final Binding taskRunning; - private final Binding taskScheduled; - private final Binding taskFinished; - private final Binding taskNotStarted; - private final Binding taskSucceeded; - private final Binding taskFailed; - private final Binding taskCancelled; + private final EasyObservableList results; + private final ObjectProperty check; + private final OptionalBinding checkState; + private final Binding checkName; + private final Binding checkRunning; + private final Binding checkScheduled; + private final Binding checkFinished; + private final Binding checkSkipped; + private final Binding checkSucceeded; + private final Binding checkFailed; + private final Binding checkCancelled; private final Binding countOfWarnSeverity; private final Binding countOfCritSeverity; + private final Binding warnOrCritsExist; + private final ResultListCellFactory resultListCellFactory; - public ListView resultsListView; + public ListView resultsListView; private Subscription resultSubscription; @Inject - public CheckDetailController(ObjectProperty selectedTask, ResultListCellFactory resultListCellFactory) { - this.results = EasyBind.wrapList(FXCollections.observableArrayList()); - this.taskState = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::stateProperty); - this.taskName = EasyBind.wrapNullable(selectedTask).map(HealthCheckTask::getTitle).orElse(""); - this.taskDuration = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::durationInMillisProperty).orElse(-1L); + public CheckDetailController(ObjectProperty selectedTask, ResultListCellFactory resultListCellFactory) { this.resultListCellFactory = resultListCellFactory; - this.taskRunning = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::runningProperty).orElse(false); //TODO: DOES NOT WORK - this.taskScheduled = taskState.map(Worker.State.SCHEDULED::equals).orElse(false); - this.taskNotStarted = taskState.map(Worker.State.READY::equals).orElse(false); - this.taskSucceeded = taskState.map(Worker.State.SUCCEEDED::equals).orElse(false); - this.taskFailed = taskState.map(Worker.State.FAILED::equals).orElse(false); - this.taskCancelled = taskState.map(Worker.State.CANCELLED::equals).orElse(false); - this.taskFinished = EasyBind.combine(taskSucceeded, taskFailed, taskCancelled, (a, b, c) -> a || b || c); + this.results = EasyBind.wrapList(FXCollections.observableArrayList()); + this.check = selectedTask; + this.checkState = EasyBind.wrapNullable(selectedTask).mapObservable(Check::stateProperty); + this.checkName = EasyBind.wrapNullable(selectedTask).map(Check::getName).orElse(""); + this.checkRunning = checkState.map(Check.CheckState.RUNNING::equals).orElse(false); + this.checkScheduled = checkState.map(Check.CheckState.SCHEDULED::equals).orElse(false); + this.checkSkipped = checkState.map(Check.CheckState.SKIPPED::equals).orElse(false); + this.checkSucceeded = checkState.map(Check.CheckState.SUCCEEDED::equals).orElse(false); + this.checkFailed = checkState.map(Check.CheckState.ERROR::equals).orElse(false); + this.checkCancelled = checkState.map(Check.CheckState.CANCELLED::equals).orElse(false); + this.checkFinished = EasyBind.combine(checkSucceeded, checkFailed, checkCancelled, (a, b, c) -> a || b || c); this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN)); 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) ); selectedTask.addListener(this::selectedTaskChanged); } - private void selectedTaskChanged(ObservableValue observable, HealthCheckTask oldValue, HealthCheckTask newValue) { + private void selectedTaskChanged(ObservableValue observable, Check oldValue, Check newValue) { if (resultSubscription != null) { resultSubscription.unsubscribe(); } if (newValue != null) { - resultSubscription = EasyBind.bindContent(results, newValue.results()); + resultSubscription = EasyBind.bindContent(results, newValue.getResults()); } } - private Function, Long> countSeverity(DiagnosticResult.Severity severity) { - return stream -> stream.filter(item -> severity.equals(item.getServerity())).count(); + private Function, Long> countSeverity(DiagnosticResult.Severity severity) { + return stream -> stream.filter(item -> severity.equals(item.diagnosis().getSeverity())).count(); } @FXML @@ -79,20 +80,12 @@ public class CheckDetailController implements FxController { /* Getter/Setter */ - public String getTaskName() { - return taskName.getValue(); + public String getCheckName() { + return checkName.getValue(); } - public Binding taskNameProperty() { - return taskName; - } - - public Number getTaskDuration() { - return taskDuration.getValue(); - } - - public Binding taskDurationProperty() { - return taskDuration; + public Binding checkNameProperty() { + return checkName; } public long getCountOfWarnSeverity() { @@ -111,60 +104,75 @@ public class CheckDetailController implements FxController { return countOfCritSeverity; } - public boolean isTaskRunning() { - return taskRunning.getValue(); + public boolean isCheckRunning() { + return checkRunning.getValue(); } - public Binding taskRunningProperty() { - return taskRunning; + public Binding checkRunningProperty() { + return checkRunning; } - public boolean isTaskFinished() { - return taskFinished.getValue(); + public boolean isCheckFinished() { + return checkFinished.getValue(); } - public Binding taskFinishedProperty() { - return taskFinished; + public Binding checkFinishedProperty() { + return checkFinished; } - public boolean isTaskScheduled() { - return taskScheduled.getValue(); + public boolean isCheckScheduled() { + return checkScheduled.getValue(); } - public Binding taskScheduledProperty() { - return taskScheduled; + public Binding checkScheduledProperty() { + return checkScheduled; } - public boolean isTaskNotStarted() { - return taskNotStarted.getValue(); + public boolean isCheckSkipped() { + return checkSkipped.getValue(); } - public Binding taskNotStartedProperty() { - return taskNotStarted; + public Binding checkSkippedProperty() { + return checkSkipped; } - public boolean isTaskSucceeded() { - return taskSucceeded.getValue(); + public boolean isCheckSucceeded() { + return checkSucceeded.getValue(); } - public Binding taskSucceededProperty() { - return taskSucceeded; + public Binding checkSucceededProperty() { + return checkSucceeded; } - public boolean isTaskFailed() { - return taskFailed.getValue(); + public boolean isCheckFailed() { + return checkFailed.getValue(); } - public Binding taskFailedProperty() { - return taskFailed; + public Binding checkFailedProperty() { + return checkFailed; } - public boolean isTaskCancelled() { - return taskCancelled.getValue(); + public boolean isCheckCancelled() { + return checkCancelled.getValue(); } - public Binding taskCancelledProperty() { - return taskCancelled; + public Binding warnOrCritsExistProperty() { + return warnOrCritsExist; } + public boolean isWarnOrCritsExist() { + return warnOrCritsExist.getValue(); + } + + public Binding checkCancelledProperty() { + return checkCancelled; + } + + public ObjectProperty checkProperty() { + return check; + } + + public Check getCheck() { + return check.get(); + } } diff --git a/src/main/java/org/cryptomator/ui/health/CheckExecutor.java b/src/main/java/org/cryptomator/ui/health/CheckExecutor.java new file mode 100644 index 000000000..5b14bd17c --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/CheckExecutor.java @@ -0,0 +1,109 @@ +package org.cryptomator.ui.health; + +import com.google.common.collect.Comparators; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.Masterkey; + +import javax.inject.Inject; +import javafx.application.Platform; +import javafx.concurrent.Task; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.util.List; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.atomic.AtomicReference; + +@HealthCheckScoped +public class CheckExecutor { + + private final Path vaultPath; + private final SecureRandom csprng; + private final Masterkey masterkey; + private final VaultConfig vaultConfig; + private final ExecutorService sequentialExecutor; + private final BlockingDeque tasksToExecute; + + + @Inject + public CheckExecutor(@HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng) { + this.vaultPath = vault.getPath(); + this.masterkey = masterkeyRef.get(); + this.vaultConfig = vaultConfigRef.get(); + this.csprng = csprng; + this.tasksToExecute = new LinkedBlockingDeque<>(); + this.sequentialExecutor = Executors.newSingleThreadExecutor(); + } + + public synchronized void executeBatch(List checks) { + checks.stream().map(c -> { + c.setState(Check.CheckState.SCHEDULED); + var task = new CheckTask(c); + tasksToExecute.addLast(task); + return task; + }).forEach(sequentialExecutor::submit); + } + + public synchronized void cancel() { + CheckTask task; + while ((task = tasksToExecute.pollLast()) != null) { + task.cancel(true); + } + } + + private class CheckTask extends Task { + + private final Check c; + private DiagnosticResult.Severity highestResultSeverity = DiagnosticResult.Severity.GOOD; + + CheckTask(Check c) { + this.c = c; + } + + @Override + protected Void call() throws Exception { + try (var masterkeyClone = masterkey.clone(); // + var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) { + c.getHealthCheck().check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> { + Platform.runLater(() -> c.getResults().add(Result.create(diagnosis))); + highestResultSeverity = Comparators.max(highestResultSeverity, diagnosis.getSeverity()); + }); + } + return null; + } + + @Override + protected void running() { + c.setState(Check.CheckState.RUNNING); + } + + @Override + protected void cancelled() { + c.setState(Check.CheckState.CANCELLED); + } + + @Override + protected void succeeded() { + c.setState(Check.CheckState.SUCCEEDED); + c.setHighestResultSeverity(highestResultSeverity); + } + + @Override + protected void failed() { + c.setState(Check.CheckState.ERROR); + c.setError(this.getException()); + } + + @Override + protected void done() { + tasksToExecute.remove(this); + } + + } + +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/health/CheckListCell.java b/src/main/java/org/cryptomator/ui/health/CheckListCell.java deleted file mode 100644 index 78f8b1b33..000000000 --- a/src/main/java/org/cryptomator/ui/health/CheckListCell.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.cryptomator.ui.health; - -import org.cryptomator.ui.controls.FontAwesome5Icon; -import org.cryptomator.ui.controls.FontAwesome5IconView; - -import javafx.beans.binding.Bindings; -import javafx.beans.value.ObservableValue; -import javafx.concurrent.Worker; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.ContentDisplay; -import javafx.scene.control.ListCell; - -class CheckListCell extends ListCell { - - private final FontAwesome5IconView stateIcon = new FontAwesome5IconView(); - - CheckListCell() { - setPadding(new Insets(6)); - setAlignment(Pos.CENTER_LEFT); - setContentDisplay(ContentDisplay.LEFT); - } - - @Override - protected void updateItem(HealthCheckTask item, boolean empty) { - super.updateItem(item, empty); - - if (item != null) { - textProperty().bind(item.titleProperty()); - item.stateProperty().addListener(this::stateChanged); - graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()),item.stateProperty())); - stateIcon.setGlyph(glyphForState(item.getState())); - } else { - textProperty().unbind(); - graphicProperty().unbind(); - setGraphic(null); - setText(null); - } - } - - private void stateChanged(ObservableValue observable, Worker.State oldState, Worker.State newState) { - stateIcon.setGlyph(glyphForState(newState)); - stateIcon.setVisible(true); - } - - private Node graphicForState(Worker.State state) { - return switch (state) { - case READY -> null; - case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon; - }; - } - - private FontAwesome5Icon glyphForState(Worker.State state) { - return switch (state) { - case READY -> FontAwesome5Icon.COG; //just a placeholder - case SCHEDULED -> FontAwesome5Icon.CLOCK; - case RUNNING -> FontAwesome5Icon.SPINNER; - case FAILED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; - case CANCELLED -> FontAwesome5Icon.BAN; - case SUCCEEDED -> FontAwesome5Icon.CHECK; - }; - } -} diff --git a/src/main/java/org/cryptomator/ui/health/CheckListCellController.java b/src/main/java/org/cryptomator/ui/health/CheckListCellController.java new file mode 100644 index 000000000..799b73358 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/CheckListCellController.java @@ -0,0 +1,70 @@ +package org.cryptomator.ui.health; + +import com.tobiasdiez.easybind.EasyBind; +import com.tobiasdiez.easybind.Subscription; +import org.cryptomator.ui.common.FxController; + +import javax.inject.Inject; +import javafx.beans.binding.Binding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.control.CheckBox; +import java.util.ArrayList; +import java.util.List; + +public class CheckListCellController implements FxController { + + + private final ObjectProperty check; + private final Binding checkName; + private final Binding checkRunnable; + private final List subscriptions; + + /* FXML */ + public CheckBox forRunSelectedCheckBox; + + @Inject + public CheckListCellController() { + check = new SimpleObjectProperty<>(); + checkRunnable = EasyBind.wrapNullable(check).mapObservable(Check::stateProperty).map(Check.CheckState.RUNNABLE::equals).orElse(false); + checkName = EasyBind.wrapNullable(check).map(Check::getName).orElse(""); + subscriptions = new ArrayList<>(); + } + + public void initialize() { + subscriptions.add(EasyBind.subscribe(check, c -> { + forRunSelectedCheckBox.selectedProperty().unbind(); + if (c != null) { + forRunSelectedCheckBox.selectedProperty().bindBidirectional(c.chosenForExecutionProperty()); + } + })); + } + + public ObjectProperty checkProperty() { + return check; + } + + public Check getCheck() { + return check.get(); + } + + public void setCheck(Check c) { + check.set(c); + } + + public Binding checkNameProperty() { + return checkName; + } + + public String getCheckName() { + return checkName.getValue(); + } + + public Binding checkRunnableProperty() { + return checkRunnable; + } + + public boolean isCheckRunnable() { + return checkRunnable.getValue(); + } +} diff --git a/src/main/java/org/cryptomator/ui/health/CheckListCellFactory.java b/src/main/java/org/cryptomator/ui/health/CheckListCellFactory.java new file mode 100644 index 000000000..d8ccc8d48 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/CheckListCellFactory.java @@ -0,0 +1,58 @@ +package org.cryptomator.ui.health; + +import org.cryptomator.ui.common.FxmlLoaderFactory; + +import javax.inject.Inject; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.util.Callback; +import java.io.IOException; +import java.io.UncheckedIOException; + +// unscoped because each cell needs its own controller +public class CheckListCellFactory implements Callback, ListCell> { + + private final FxmlLoaderFactory fxmlLoaders; + + @Inject + CheckListCellFactory(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) { + this.fxmlLoaders = fxmlLoaders; + } + + @Override + public ListCell call(ListView param) { + try { + FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_check_listcell.fxml"); + return new CheckListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController()); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load /fxml/health_check_listcell.fxml.", e); + } + } + + private static class Cell extends ListCell { + + private final Parent node; + private final CheckListCellController controller; + + public Cell(Parent node, CheckListCellController controller) { + this.node = node; + this.controller = controller; + } + + @Override + protected void updateItem(Check item, boolean empty) { + super.updateItem(item, empty); + if (item == null || empty) { + setText(null); + setGraphic(null); + } else { + controller.setCheck(item); + setContentDisplay(ContentDisplay.GRAPHIC_ONLY); + setGraphic(node); + } + } + } +} diff --git a/src/main/java/org/cryptomator/ui/health/CheckListController.java b/src/main/java/org/cryptomator/ui/health/CheckListController.java index ccb41d56b..75ecdef52 100644 --- a/src/main/java/org/cryptomator/ui/health/CheckListController.java +++ b/src/main/java/org/cryptomator/ui/health/CheckListController.java @@ -1,7 +1,6 @@ package org.cryptomator.ui.health; import com.google.common.base.Preconditions; -import com.tobiasdiez.easybind.EasyBind; import dagger.Lazy; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; @@ -9,147 +8,113 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javafx.beans.binding.Binding; +import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.IntegerProperty; +import javafx.beans.binding.IntegerBinding; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleIntegerProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import javafx.concurrent.Worker; +import javafx.collections.transformation.FilteredList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; import javafx.scene.control.ListView; -import javafx.scene.control.cell.CheckBoxListCell; +import javafx.scene.control.SelectionMode; import javafx.stage.Stage; -import javafx.util.StringConverter; import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutorService; +import java.util.List; @HealthCheckScoped public class CheckListController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(CheckListController.class); - private static final Set END_STATES = Set.of(Worker.State.FAILED, Worker.State.CANCELLED, Worker.State.SUCCEEDED); private final Stage window; - private final ObservableList tasks; + private final ObservableList checks; + private final CheckExecutor checkExecutor; + private final FilteredList chosenChecks; private final ReportWriter reportWriter; - private final ExecutorService executorService; - private final ObjectProperty selectedTask; - private final Lazy errorComponenBuilder; - private final SimpleObjectProperty> runningTask; - private final Binding running; - private final Binding finished; - private final Map listPickIndicators; - private final IntegerProperty numberOfPickedChecks; + private final ObjectProperty selectedCheck; + private final BooleanBinding mainRunStarted; //TODO: rerunning not considered for now + private final BooleanBinding somethingsRunning; + private final Lazy errorComponentBuilder; + private final IntegerBinding chosenTaskCount; private final BooleanBinding anyCheckSelected; - private final BooleanProperty showResultScreen; + private final CheckListCellFactory listCellFactory; /* FXML */ - public ListView checksListView; - + public ListView checksListView; @Inject - public CheckListController(@HealthCheckWindow Stage window, Lazy> tasks, ReportWriter reportWriteTask, ObjectProperty selectedTask, ExecutorService executorService, Lazy errorComponenBuilder) { + public CheckListController(@HealthCheckWindow Stage window, List checks, CheckExecutor checkExecutor, ReportWriter reportWriteTask, ObjectProperty selectedCheck, Lazy errorComponentBuilder, CheckListCellFactory listCellFactory) { this.window = window; - this.tasks = FXCollections.observableArrayList(tasks.get()); + this.checks = FXCollections.observableList(checks, Check::observables); + this.checkExecutor = checkExecutor; + this.listCellFactory = listCellFactory; + this.chosenChecks = this.checks.filtered(Check::isChosenForExecution); this.reportWriter = reportWriteTask; - this.executorService = executorService; - this.selectedTask = selectedTask; - this.errorComponenBuilder = errorComponenBuilder; - this.runningTask = new SimpleObjectProperty<>(); - this.running = EasyBind.wrapNullable(runningTask).mapObservable(Worker::runningProperty).orElse(false); - this.finished = EasyBind.wrapNullable(runningTask).mapObservable(Worker::stateProperty).map(END_STATES::contains).orElse(false); - this.listPickIndicators = new HashMap<>(); - this.numberOfPickedChecks = new SimpleIntegerProperty(0); - this.tasks.forEach(task -> { - var entrySelectedProp = new SimpleBooleanProperty(false); - entrySelectedProp.addListener((observable, oldValue, newValue) -> numberOfPickedChecks.set(numberOfPickedChecks.get() + (newValue ? 1 : -1))); - listPickIndicators.put(task, entrySelectedProp); - }); - this.anyCheckSelected = selectedTask.isNotNull(); - this.showResultScreen = new SimpleBooleanProperty(false); + this.selectedCheck = selectedCheck; + this.errorComponentBuilder = errorComponentBuilder; + this.chosenTaskCount = Bindings.size(this.chosenChecks); + this.mainRunStarted = Bindings.isEmpty(this.checks.filtered(c -> c.getState() == Check.CheckState.RUNNABLE)); + this.somethingsRunning = Bindings.isNotEmpty(this.checks.filtered(c -> c.getState() == Check.CheckState.SCHEDULED || c.getState() == Check.CheckState.RUNNING)); + this.anyCheckSelected = selectedCheck.isNotNull(); } @FXML public void initialize() { - checksListView.setItems(tasks); - checksListView.setCellFactory(CheckBoxListCell.forListView(listPickIndicators::get, new StringConverter() { - @Override - public String toString(HealthCheckTask object) { - return object.getTitle(); - } - - @Override - public HealthCheckTask fromString(String string) { - return null; - } - })); - selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty()); + checksListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + checksListView.setItems(checks); + checksListView.setCellFactory(listCellFactory); + selectedCheck.bind(checksListView.getSelectionModel().selectedItemProperty()); } @FXML - public void toggleSelectAll(ActionEvent event) { - if (event.getSource() instanceof CheckBox c) { - listPickIndicators.forEach( (task, pickProperty) -> pickProperty.set(c.isSelected())); - } + public void selectAllChecks() { + checks.forEach(t -> t.chosenForExecutionProperty().set(true)); + } + + @FXML + public void deselectAllChecks() { + checks.forEach(t -> t.chosenForExecutionProperty().set(false)); } @FXML public void runSelectedChecks() { - Preconditions.checkState(runningTask.get() == null); - var batch = checksListView.getItems().filtered(item -> listPickIndicators.get(item).get()); - var batchService = new BatchService(batch); - batchService.setExecutor(executorService); - batchService.start(); - runningTask.set(batchService); - showResultScreen.set(true); - checksListView.getSelectionModel().select(batch.get(0)); - checksListView.setCellFactory(view -> new CheckListCell()); + Preconditions.checkState(!mainRunStarted.get()); + Preconditions.checkState(!somethingsRunning.get()); + Preconditions.checkState(!chosenChecks.isEmpty()); + + checks.filtered(c -> !c.isChosenForExecution()).forEach(c -> c.setState(Check.CheckState.SKIPPED)); + checkExecutor.executeBatch(chosenChecks); + checksListView.getSelectionModel().select(chosenChecks.get(0)); + checksListView.refresh(); window.sizeToScene(); } @FXML - public synchronized void cancelCheck() { - Preconditions.checkState(runningTask.get() != null); - runningTask.get().cancel(); + public synchronized void cancelRun() { + Preconditions.checkState(somethingsRunning.get()); + checkExecutor.cancel(); } @FXML public void exportResults() { try { - reportWriter.writeReport(tasks); + reportWriter.writeReport(chosenChecks); } catch (IOException e) { LOG.error("Failed to write health check report.", e); - errorComponenBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); + errorComponentBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); } } /* Getter/Setter */ public boolean isRunning() { - return running.getValue(); + return somethingsRunning.getValue(); } - public Binding runningProperty() { - return running; - } - - public boolean isFinished() { - return finished.getValue(); - } - - public Binding finishedProperty() { - return finished; + public BooleanBinding runningProperty() { + return somethingsRunning; } public boolean isAnyCheckSelected() { @@ -160,21 +125,20 @@ public class CheckListController implements FxController { return anyCheckSelected; } - public boolean getShowResultScreen() { - return showResultScreen.get(); + public boolean isMainRunStarted() { + return mainRunStarted.get(); } - public BooleanProperty showResultScreenProperty() { - return showResultScreen; + public BooleanBinding mainRunStartedProperty() { + return mainRunStarted; } - public int getNumberOfPickedChecks() { - return numberOfPickedChecks.get(); + public int getChosenTaskCount() { + return chosenTaskCount.getValue(); } - public IntegerProperty numberOfPickedChecksProperty() { - return numberOfPickedChecks; + public IntegerBinding chosenTaskCountProperty() { + return chosenTaskCount; } - } diff --git a/src/main/java/org/cryptomator/ui/health/CheckStateIconView.java b/src/main/java/org/cryptomator/ui/health/CheckStateIconView.java new file mode 100644 index 000000000..4f1a35f7a --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/CheckStateIconView.java @@ -0,0 +1,83 @@ +package org.cryptomator.ui.health; + +import com.tobiasdiez.easybind.EasyBind; +import com.tobiasdiez.easybind.Subscription; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.ui.common.Animations; +import org.cryptomator.ui.common.AutoAnimator; +import org.cryptomator.ui.controls.FontAwesome5Icon; +import org.cryptomator.ui.controls.FontAwesome5IconView; + +import javafx.beans.binding.Bindings; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableObjectValue; +import java.util.List; + +/** + * A {@link FontAwesome5IconView} that automatically sets the glyph depending on + * the {@link Check#stateProperty() state} and {@link Check#highestResultSeverityProperty() severity} of a HealthCheck. + */ +public class CheckStateIconView extends FontAwesome5IconView { + + private final ObjectProperty check = new SimpleObjectProperty<>(); + private final ObservableObjectValue state; + private final ObservableObjectValue severity; + private final List subscriptions; + private final AutoAnimator onRunningRotator; + + public CheckStateIconView() { + this.state = EasyBind.wrapNullable(check).mapObservable(Check::stateProperty).asOrdinary(); + this.severity = EasyBind.wrapNullable(check).mapObservable(Check::highestResultSeverityProperty).asOrdinary(); + this.glyph.bind(Bindings.createObjectBinding(this::glyphForState, state, severity)); + this.subscriptions = List.of( // + EasyBind.includeWhen(getStyleClass(), "glyph-icon-muted", Bindings.equal(state, Check.CheckState.SKIPPED).or(Bindings.equal(state, Check.CheckState.CANCELLED))), // + EasyBind.includeWhen(getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), // + EasyBind.includeWhen(getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN).or(Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL))), // + EasyBind.includeWhen(getStyleClass(), "glyph-icon-red", Bindings.equal(state, Check.CheckState.ERROR)) // + ); + var animation = Animations.createDiscrete360Rotation(this); + this.onRunningRotator = AutoAnimator.animate(animation) // + .onCondition(Bindings.equal(state, Check.CheckState.RUNNING)) // + .afterStop(() -> setRotate(0)) // + .build(); + } + + private FontAwesome5Icon glyphForState() { + if (state.getValue() == null) { + return null; + } + return switch (state.getValue()) { + case RUNNABLE -> null; + case SKIPPED -> FontAwesome5Icon.FAST_FORWARD; + case SCHEDULED -> FontAwesome5Icon.CLOCK; + case RUNNING -> FontAwesome5Icon.SPINNER; + case ERROR -> FontAwesome5Icon.TIMES; + case CANCELLED -> FontAwesome5Icon.BAN; + case SUCCEEDED -> glyphIconForSeverity(); + }; + } + + private FontAwesome5Icon glyphIconForSeverity() { + if (severity.getValue() == null) { + return null; + } + return switch (severity.getValue()) { + case GOOD, INFO -> FontAwesome5Icon.CHECK; + case WARN, CRITICAL -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; + }; + } + + public ObjectProperty checkProperty() { + return check; + } + + public void setCheck(Check c) { + check.set(c); + } + + public Check getCheck() { + return check.get(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java b/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java index 48b16f694..f78e815c6 100644 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java +++ b/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java @@ -4,9 +4,11 @@ import dagger.BindsInstance; import dagger.Lazy; import dagger.Subcomponent; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; +import javax.inject.Named; import javafx.scene.Scene; import javafx.stage.Stage; @@ -14,15 +16,26 @@ import javafx.stage.Stage; @Subcomponent(modules = {HealthCheckModule.class}) public interface HealthCheckComponent { + LoadUnverifiedConfigResult loadConfig(); + @HealthCheckWindow Stage window(); @FxmlScene(FxmlFile.HEALTH_START) - Lazy scene(); + Lazy startScene(); + + @FxmlScene(FxmlFile.HEALTH_START_FAIL) + Lazy failScene(); default Stage showHealthCheckWindow() { Stage stage = window(); - stage.setScene(scene().get()); + // TODO reevaluate config loading, as soon as we have the new generic error screen + var unverifiedConf = loadConfig(); + if (unverifiedConf.config() != null) { + stage.setScene(startScene().get()); + } else { + stage.setScene(failScene().get()); + } stage.show(); return stage; } @@ -33,7 +46,11 @@ public interface HealthCheckComponent { @BindsInstance Builder vault(@HealthCheckWindow Vault vault); + @BindsInstance + Builder owner(@Named("healthCheckOwner") Stage owner); + HealthCheckComponent build(); } + record LoadUnverifiedConfigResult(VaultConfig.UnverifiedVaultConfig config, Throwable error) {} } diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java index e33a9f2f1..ad5ac6156 100644 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java +++ b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java @@ -17,8 +17,8 @@ import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.StageFactory; import org.cryptomator.ui.keyloading.KeyLoadingComponent; import org.cryptomator.ui.keyloading.KeyLoadingStrategy; -import org.cryptomator.ui.mainwindow.MainWindow; +import javax.inject.Named; import javax.inject.Provider; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -26,8 +26,9 @@ import javafx.beans.value.ChangeListener; import javafx.scene.Scene; import javafx.stage.Modality; import javafx.stage.Stage; -import java.security.SecureRandom; -import java.util.Collection; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; @@ -36,6 +37,18 @@ import java.util.concurrent.atomic.AtomicReference; @Module(subcomponents = {KeyLoadingComponent.class}) abstract class HealthCheckModule { + // TODO reevaluate config loading, as soon as we have the new generic error screen + @Provides + @HealthCheckScoped + static HealthCheckComponent.LoadUnverifiedConfigResult provideLoadConfigResult(@HealthCheckWindow Vault vault) { + try { + return new HealthCheckComponent.LoadUnverifiedConfigResult(vault.getUnverifiedVaultConfig(), null); + } catch (IOException e) { + return new HealthCheckComponent.LoadUnverifiedConfigResult(null, e); + } + } + + @Provides @HealthCheckScoped static AtomicReference provideMasterkeyRef() { @@ -50,27 +63,20 @@ abstract class HealthCheckModule { @Provides @HealthCheckScoped - static Collection provideAvailableHealthChecks() { - return HealthCheck.allChecks(); - } - - @Provides - @HealthCheckScoped - static ObjectProperty provideSelectedHealthCheckTask() { + static ObjectProperty provideSelectedCheck() { return new SimpleObjectProperty<>(); } - /* Only inject with Lazy-Wrapper!*/ @Provides @HealthCheckScoped - static Collection provideAvailableHealthCheckTasks(Collection availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) { - return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList(); + static List provideAvailableChecks() { + return HealthCheck.allChecks().stream().map(Check::new).toList(); } @Provides @HealthCheckWindow @HealthCheckScoped - static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @HealthCheckWindow Stage window) { + static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) { return compBuilder.vault(vault).window(window).build().keyloadingStrategy(); } @@ -81,15 +87,27 @@ abstract class HealthCheckModule { return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle); } + @Provides + @Named("unlockWindow") + @HealthCheckScoped + static Stage provideUnlockWindow (@HealthCheckWindow Stage window, @HealthCheckWindow Vault vault, StageFactory factory, ResourceBundle resourceBundle) { + Stage stage = factory.create(); + stage.initModality(Modality.WINDOW_MODAL); + stage.initOwner(window); + stage.setTitle(String.format(resourceBundle.getString("unlock.title"), vault.getDisplayName())); + stage.setResizable(false); + return stage; + } + @Provides @HealthCheckWindow @HealthCheckScoped - static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener showingListener) { + static Stage provideStage(StageFactory factory, @Named("healthCheckOwner") Stage owner, @HealthCheckWindow Vault vault, ChangeListener showingListener, ResourceBundle resourceBundle) { Stage stage = factory.create(); - stage.setTitle(resourceBundle.getString("health.title")); - stage.setResizable(true); stage.initModality(Modality.WINDOW_MODAL); stage.initOwner(owner); + stage.setTitle(String.format(resourceBundle.getString("health.title"), vault.getDisplayName())); + stage.setResizable(true); stage.showingProperty().addListener(showingListener); // bind masterkey lifecycle to window return stage; } @@ -111,6 +129,13 @@ abstract class HealthCheckModule { return fxmlLoaders.createScene(FxmlFile.HEALTH_START); } + @Provides + @FxmlScene(FxmlFile.HEALTH_START_FAIL) + @HealthCheckScoped + static Scene provideHealthStartFailScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.HEALTH_START_FAIL); + } + @Provides @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) @HealthCheckScoped @@ -123,6 +148,11 @@ abstract class HealthCheckModule { @FxControllerKey(StartController.class) abstract FxController bindStartController(StartController controller); + @Binds + @IntoMap + @FxControllerKey(StartFailController.class) + abstract FxController bindStartFailController(StartFailController controller); + @Binds @IntoMap @FxControllerKey(CheckListController.class) @@ -138,4 +168,8 @@ abstract class HealthCheckModule { @FxControllerKey(ResultListCellController.class) abstract FxController bindResultListCellController(ResultListCellController controller); + @Binds + @IntoMap + @FxControllerKey(CheckListCellController.class) + abstract FxController bindCheckListCellController(CheckListCellController controller); } diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java b/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java deleted file mode 100644 index 7acbfc1c2..000000000 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.cryptomator.ui.health; - -import org.cryptomator.cryptofs.VaultConfig; -import org.cryptomator.cryptofs.health.api.DiagnosticResult; -import org.cryptomator.cryptofs.health.api.HealthCheck; -import org.cryptomator.cryptolib.api.Masterkey; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javafx.application.Platform; -import javafx.beans.property.LongProperty; -import javafx.beans.property.SimpleLongProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.concurrent.Task; -import java.nio.file.Path; -import java.security.SecureRandom; -import java.time.Duration; -import java.time.Instant; -import java.util.MissingResourceException; -import java.util.Objects; -import java.util.ResourceBundle; -import java.util.concurrent.CancellationException; - -class HealthCheckTask extends Task { - - private static final Logger LOG = LoggerFactory.getLogger(HealthCheckTask.class); - - private final Path vaultPath; - private final VaultConfig vaultConfig; - private final Masterkey masterkey; - private final SecureRandom csprng; - private final HealthCheck check; - private final ObservableList results; - private final LongProperty durationInMillis; - - public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, ResourceBundle resourceBundle) { - this.vaultPath = Objects.requireNonNull(vaultPath); - this.vaultConfig = Objects.requireNonNull(vaultConfig); - this.masterkey = Objects.requireNonNull(masterkey); - this.csprng = Objects.requireNonNull(csprng); - this.check = Objects.requireNonNull(check); - this.results = FXCollections.observableArrayList(); - try { - updateTitle(resourceBundle.getString("health." + check.identifier())); - } catch (MissingResourceException e) { - LOG.warn("Missing proper name for health check {}, falling back to default.", check.identifier()); - updateTitle(check.identifier()); - } - this.durationInMillis = new SimpleLongProperty(-1); - } - - @Override - protected Void call() { - Instant start = Instant.now(); - try (var masterkeyClone = masterkey.clone(); // - var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) { - check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, result -> { - if (isCancelled()) { - throw new CancellationException(); - } - // FIXME: slowdown for demonstration purposes only: - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - if (isCancelled()) { - return; - } else { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - } - Platform.runLater(() -> results.add(result)); - }); - } - Platform.runLater(() ->durationInMillis.set(Duration.between(start, Instant.now()).toMillis())); - return null; - } - - @Override - protected void scheduled() { - LOG.info("starting {}", check.identifier()); - } - - @Override - protected void done() { - LOG.info("finished {}", check.identifier()); - } - - /* Getter */ - - public ObservableList results() { - return results; - } - - public HealthCheck getCheck() { - return check; - } - - public LongProperty durationInMillisProperty() { - return durationInMillis; - } - - public long getDurationInMillis() { - return durationInMillis.get(); - } - -} diff --git a/src/main/java/org/cryptomator/ui/health/ReportWriter.java b/src/main/java/org/cryptomator/ui/health/ReportWriter.java index fb74cbd51..18b785e91 100644 --- a/src/main/java/org/cryptomator/ui/health/ReportWriter.java +++ b/src/main/java/org/cryptomator/ui/health/ReportWriter.java @@ -1,15 +1,12 @@ package org.cryptomator.ui.health; -import org.apache.commons.lang3.exception.ExceptionUtils; +import com.google.common.base.Throwables; import org.cryptomator.common.Environment; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.VaultConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.application.Application; -import javafx.concurrent.Worker; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStreamWriter; @@ -28,11 +25,10 @@ import java.util.stream.Collectors; @HealthCheckScoped public class ReportWriter { - private static final Logger LOG = LoggerFactory.getLogger(ReportWriter.class); private static final String REPORT_HEADER = """ - ************************************** - * Cryptomator Vault Health Report * - ************************************** + ******************************************* + * Cryptomator Vault Health Report * + ******************************************* Analyzed vault: %s (Current name "%s") Vault storage path: %s """; @@ -58,38 +54,35 @@ public class ReportWriter { this.exportDestination = env.getLogDir().orElse(Path.of(System.getProperty("user.home"))).resolve("healthReport_" + vault.getDisplayName() + "_" + TIME_STAMP.format(Instant.now()) + ".log"); } - protected void writeReport(Collection tasks) throws IOException { + protected void writeReport(Collection performedChecks) throws IOException { try (var out = Files.newOutputStream(exportDestination, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); // var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) { writer.write(REPORT_HEADER.formatted(vaultConfig.getId(), vault.getDisplayName(), vault.getPath())); - for (var task : tasks) { - if (task.getState() == Worker.State.READY) { - LOG.debug("Skipping not performed check {}.", task.getCheck().identifier()); - continue; - } - writer.write(REPORT_CHECK_HEADER.formatted(task.getCheck().identifier())); - switch (task.getState()) { + for (var check : performedChecks) { + writer.write(REPORT_CHECK_HEADER.formatted(check.getHealthCheck().name())); + switch (check.getState()) { case SUCCEEDED -> { writer.write("STATUS: SUCCESS\nRESULTS:\n"); - for (var result : task.results()) { - writer.write(REPORT_CHECK_RESULT.formatted(result.getServerity(), result.toString())); + for (var result : check.getResults()) { + writer.write(REPORT_CHECK_RESULT.formatted(result.diagnosis().getSeverity(), result.getDescription())); } } case CANCELLED -> writer.write("STATUS: CANCELED\n"); - case FAILED -> { - writer.write("STATUS: FAILED\nREASON:\n" + task.getCheck().identifier()); - writer.write(prepareFailureMsg(task)); + case ERROR -> { + writer.write("STATUS: FAILED\nREASON:\n"); + writer.write(prepareFailureMsg(check)); } - case RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running."); + case RUNNABLE, RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running."); + case SKIPPED -> {} //noop } } } reveal(); } - private String prepareFailureMsg(HealthCheckTask task) { - if (task.getException() != null) { - return ExceptionUtils.getStackTrace(task.getException()) // + private String prepareFailureMsg(Check check) { + if (check.getError() != null) { + return Throwables.getStackTraceAsString(check.getError()) // .lines() // .map(line -> "\t\t" + line + "\n") // .collect(Collectors.joining()); diff --git a/src/main/java/org/cryptomator/ui/health/Result.java b/src/main/java/org/cryptomator/ui/health/Result.java new file mode 100644 index 000000000..8327a1130 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/Result.java @@ -0,0 +1,40 @@ +package org.cryptomator.ui.health; + +import org.cryptomator.cryptofs.health.api.DiagnosticResult; + +import javafx.beans.Observable; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +record Result(DiagnosticResult diagnosis, ObjectProperty fixState) { + + enum FixState { + NOT_FIXABLE, + FIXABLE, + FIXING, + FIXED, + FIX_FAILED + } + + public static Result create(DiagnosticResult diagnosis) { + FixState initialState = diagnosis.getSeverity() == DiagnosticResult.Severity.WARN ? FixState.FIXABLE : FixState.NOT_FIXABLE; + return new Result(diagnosis, new SimpleObjectProperty<>(initialState)); + } + + public Observable[] observables() { + return new Observable[]{fixState}; + } + + public String getDescription() { + return diagnosis.toString(); + } + + public FixState getState() { + return fixState.get(); + } + + public void setState(FixState state) { + this.fixState.set(state); + } + +} diff --git a/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java b/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java index 9a639e8a0..841a8f5c4 100644 --- a/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java +++ b/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java @@ -4,25 +4,30 @@ import com.google.common.base.Preconditions; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.Masterkey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javafx.scene.control.Alert; +import javafx.application.Platform; import java.nio.file.Path; import java.security.SecureRandom; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; @HealthCheckScoped class ResultFixApplier { - private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class); - private final Path vaultPath; private final SecureRandom csprng; private final Masterkey masterkey; private final VaultConfig vaultConfig; + private final ExecutorService sequentialExecutor; @Inject public ResultFixApplier(@HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng) { @@ -30,18 +35,32 @@ class ResultFixApplier { this.masterkey = masterkeyRef.get(); this.vaultConfig = vaultConfigRef.get(); this.csprng = csprng; + this.sequentialExecutor = Executors.newSingleThreadExecutor(); } - public void fix(DiagnosticResult result) { - Preconditions.checkArgument(result.getServerity() == DiagnosticResult.Severity.WARN, "Unfixable result"); + public CompletionStage fix(Result result) { + Preconditions.checkArgument(result.getState() == Result.FixState.FIXABLE); + result.setState(Result.FixState.FIXING); + return CompletableFuture.runAsync(() -> fix(result.diagnosis()), sequentialExecutor) + .whenCompleteAsync((unused, throwable) -> { + var fixed = throwable == null ? Result.FixState.FIXED : Result.FixState.FIX_FAILED; + result.setState(fixed); + }, Platform::runLater); + } + + public void fix(DiagnosticResult diagnosis) { + Preconditions.checkArgument(diagnosis.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result"); try (var masterkeyClone = masterkey.clone(); // - var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) { - result.fix(vaultPath, vaultConfig, masterkeyClone, cryptor); + var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) { + diagnosis.fix(vaultPath, vaultConfig, masterkeyClone, cryptor); } catch (Exception e) { - LOG.error("Failed to apply fix", e); - Alert alert = new Alert(Alert.AlertType.ERROR, e.getMessage()); - alert.showAndWait(); - //TODO: real error/not supported handling + throw new FixFailedException(e); + } + } + + public static class FixFailedException extends CompletionException { + private FixFailedException(Throwable cause) { + super(cause); } } } diff --git a/src/main/java/org/cryptomator/ui/health/ResultListCellController.java b/src/main/java/org/cryptomator/ui/health/ResultListCellController.java index f2bca059a..59ee2fa67 100644 --- a/src/main/java/org/cryptomator/ui/health/ResultListCellController.java +++ b/src/main/java/org/cryptomator/ui/health/ResultListCellController.java @@ -1,92 +1,212 @@ package org.cryptomator.ui.health; import com.tobiasdiez.easybind.EasyBind; +import com.tobiasdiez.easybind.Subscription; import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.ui.common.Animations; +import org.cryptomator.ui.common.AutoAnimator; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.controls.FontAwesome5Icon; import org.cryptomator.ui.controls.FontAwesome5IconView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javafx.application.Platform; import javafx.beans.binding.Binding; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.ObjectBinding; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; +import javafx.beans.value.ObservableObjectValue; import javafx.fxml.FXML; -import javafx.scene.control.Button; +import javafx.scene.control.Tooltip; +import javafx.util.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; // unscoped because each cell needs its own controller public class ResultListCellController implements FxController { - private final ResultFixApplier fixApplier; - private final ObjectProperty result; - private final Binding description; + private static final FontAwesome5Icon INFO_ICON = FontAwesome5Icon.INFO_CIRCLE; + private static final FontAwesome5Icon GOOD_ICON = FontAwesome5Icon.CHECK; + private static final FontAwesome5Icon WARN_ICON = FontAwesome5Icon.EXCLAMATION_TRIANGLE; + private static final FontAwesome5Icon CRIT_ICON = FontAwesome5Icon.TIMES; - public FontAwesome5IconView iconView; - public Button actionButton; + private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class); + + private final ObjectProperty result; + private final ObservableObjectValue severity; + private final Binding description; + private final ResultFixApplier fixApplier; + private final ObservableObjectValue fixState; + private final ObjectBinding severityGlyph; + private final ObjectBinding fixGlyph; + private final BooleanBinding fixable; + private final BooleanBinding fixing; + private final BooleanBinding fixed; + private final BooleanBinding fixFailed; + private final BooleanBinding fixRunningOrDone; + private final List subscriptions; + private final Tooltip fixSuccess; + private final Tooltip fixFail; + + private AutoAnimator fixRunningRotator; + + /* FXML */ + public FontAwesome5IconView severityView; + public FontAwesome5IconView fixView; @Inject - public ResultListCellController(ResultFixApplier fixApplier) { + public ResultListCellController(ResultFixApplier fixApplier, ResourceBundle resourceBundle) { this.result = new SimpleObjectProperty<>(null); - this.description = EasyBind.wrapNullable(result).map(DiagnosticResult::toString).orElse(""); + this.severity = EasyBind.wrapNullable(result).map(r -> r.diagnosis().getSeverity()).asOrdinary(); + this.description = EasyBind.wrapNullable(result).map(Result::getDescription).orElse(""); this.fixApplier = fixApplier; - result.addListener(this::updateCellContent); - } - - private void updateCellContent(ObservableValue observable, DiagnosticResult oldVal, DiagnosticResult newVal) { - iconView.getStyleClass().clear(); - actionButton.setVisible(false); - //TODO: see comment in case WARN - actionButton.setManaged(false); - switch (newVal.getServerity()) { - case INFO -> { - iconView.setGlyph(FontAwesome5Icon.INFO_CIRCLE); - iconView.getStyleClass().add("glyph-icon-muted"); - } - case GOOD -> { - iconView.setGlyph(FontAwesome5Icon.CHECK); - iconView.getStyleClass().add("glyph-icon-primary"); - } - case WARN -> { - iconView.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE); - iconView.getStyleClass().add("glyph-icon-orange"); - //TODO: Neither is any fix implemented, nor it is ensured, that only fix is executed at a time with good ui indication - // before both are not fix, do not show the button - //actionButton.setVisible(true); - } - case CRITICAL -> { - iconView.setGlyph(FontAwesome5Icon.TIMES); - iconView.getStyleClass().add("glyph-icon-red"); - } - } + this.fixState = EasyBind.wrapNullable(result).mapObservable(Result::fixState).asOrdinary(); + this.severityGlyph = Bindings.createObjectBinding(this::getSeverityGlyph, result); + this.fixGlyph = Bindings.createObjectBinding(this::getFixGlyph, fixState); + this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState); + this.fixing = Bindings.createBooleanBinding(this::isFixing, fixState); + this.fixed = Bindings.createBooleanBinding(this::isFixed, fixState); + this.fixFailed = Bindings.createBooleanBinding(this::isFixFailed, fixState); + this.fixRunningOrDone = fixing.or(fixed).or(fixFailed); + this.subscriptions = new ArrayList<>(); + this.fixSuccess = new Tooltip(resourceBundle.getString("health.fix.successTip")); + this.fixFail = new Tooltip(resourceBundle.getString("health.fix.failTip")); + fixSuccess.setShowDelay(Duration.millis(100)); + fixFail.setShowDelay(Duration.millis(100)); } @FXML - public void runResultAction() { - final var realResult = result.get(); - if (realResult != null) { - fixApplier.fix(realResult); + public void initialize() { + // see getGlyph() for relevant glyphs: + subscriptions.addAll(List.of(EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-muted", Bindings.equal(severity, DiagnosticResult.Severity.INFO)), // + EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), // + EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN)), // + EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-red", Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL)) // + )); + var animation = Animations.createDiscrete360Rotation(fixView); + this.fixRunningRotator = AutoAnimator.animate(animation) // + .onCondition(Bindings.equal(fixState, Result.FixState.FIXING)) // + .afterStop(() -> fixView.setRotate(0)) // + .build(); + } + + @FXML + public void fix() { + Result r = result.get(); + if (r != null) { + fixApplier.fix(r).whenCompleteAsync(this::fixFinished, Platform::runLater); } } + + private void fixFinished(Void unused, Throwable exception) { + if (exception != null) { + LOG.error("Failed to apply fix", exception); + Tooltip.install(fixView, fixFail); + } else { + Tooltip.install(fixView, fixSuccess); + } + } + + /* Getter & Setter */ - - public DiagnosticResult getResult() { + public Result getResult() { return result.get(); } - public void setResult(DiagnosticResult result) { + public void setResult(Result result) { this.result.set(result); } - public ObjectProperty resultProperty() { + public ObjectProperty resultProperty() { return result; } + public Binding descriptionProperty() { + return description; + } + public String getDescription() { return description.getValue(); } - public Binding descriptionProperty() { - return description; + public ObjectBinding severityGlyphProperty() { + return severityGlyph; } + + public FontAwesome5Icon getSeverityGlyph() { + var r = result.get(); + if (r == null) { + return null; + } + return switch (r.diagnosis().getSeverity()) { + case INFO -> INFO_ICON; + case GOOD -> GOOD_ICON; + case WARN -> WARN_ICON; + case CRITICAL -> CRIT_ICON; + }; + } + + public ObjectBinding fixGlyphProperty() { + return fixGlyph; + } + + public FontAwesome5Icon getFixGlyph() { + if (fixState.getValue() == null) { + return null; + } + return switch (fixState.getValue()) { + case NOT_FIXABLE, FIXABLE -> null; + case FIXING -> FontAwesome5Icon.SPINNER; + case FIXED -> FontAwesome5Icon.CHECK; + case FIX_FAILED -> FontAwesome5Icon.TIMES; + }; + } + + public BooleanBinding fixableProperty() { + return fixable; + } + + public boolean isFixable() { + return Result.FixState.FIXABLE.equals(fixState.get()); + } + + public BooleanBinding fixingProperty() { + return fixing; + } + + public boolean isFixing() { + return Result.FixState.FIXING.equals(fixState.get()); + } + + public BooleanBinding fixedProperty() { + return fixed; + } + + public boolean isFixed() { + return Result.FixState.FIXED.equals(fixState.get()); + } + + public BooleanBinding fixFailedProperty() { + return fixFailed; + } + + public Boolean isFixFailed() { + return Result.FixState.FIX_FAILED.equals(fixState.get()); + } + + public BooleanBinding fixRunningOrDoneProperty() { + return fixRunningOrDone; + } + + public boolean isFixRunningOrDone() { + return fixRunningOrDone.get(); + } + + } diff --git a/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java b/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java index 7acada487..86c793bf7 100644 --- a/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java +++ b/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java @@ -1,7 +1,6 @@ package org.cryptomator.ui.health; -import org.cryptomator.cryptofs.health.api.DiagnosticResult; import org.cryptomator.ui.common.FxmlLoaderFactory; import javax.inject.Inject; @@ -15,7 +14,7 @@ import java.io.IOException; import java.io.UncheckedIOException; @HealthCheckScoped -public class ResultListCellFactory implements Callback, ListCell> { +public class ResultListCellFactory implements Callback, ListCell> { private final FxmlLoaderFactory fxmlLoaders; @@ -25,7 +24,7 @@ public class ResultListCellFactory implements Callback call(ListView param) { + public ListCell call(ListView param) { try { FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_result_listcell.fxml"); return new ResultListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController()); @@ -34,7 +33,7 @@ public class ResultListCellFactory implements Callback { + private static class Cell extends ListCell { private final Parent node; private final ResultListCellController controller; @@ -45,7 +44,7 @@ public class ResultListCellFactory implements Callback unverifiedVaultConfig; + private final Stage unlockWindow; + private final ObjectProperty unverifiedVaultConfig; private final KeyLoadingStrategy keyLoadingStrategy; private final ExecutorService executor; private final AtomicReference masterkeyRef; @@ -40,29 +43,18 @@ public class StartController implements FxController { private final Lazy checkScene; private final Lazy errorComponent; - /* FXML */ - @Inject - public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy checkScene, Lazy errorComponent) { + public StartController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy checkScene, Lazy errorComponent, @Named("unlockWindow") Stage unlockWindow) { + Preconditions.checkNotNull(configLoadResult.config()); this.window = window; + this.unlockWindow = unlockWindow; + this.unverifiedVaultConfig = new SimpleObjectProperty<>(configLoadResult.config()); this.keyLoadingStrategy = keyLoadingStrategy; this.executor = executor; this.masterkeyRef = masterkeyRef; this.vaultConfigRef = vaultConfigRef; this.checkScene = checkScene; this.errorComponent = errorComponent; - - //TODO: this is ugly - //idea: delay the loading of the vault config and show a spinner (something like "check/load config") and react to the result of the loading - //or: load vault config in a previous step to see if it is loadable. - VaultConfig.UnverifiedVaultConfig tmp; - try { - tmp = vault.getUnverifiedVaultConfig(); - } catch (IOException e) { - e.printStackTrace(); - tmp = null; - } - this.unverifiedVaultConfig = Optional.ofNullable(tmp); } @FXML @@ -74,41 +66,45 @@ public class StartController implements FxController { @FXML public void next() { LOG.trace("StartController.next()"); - executor.submit(this::loadKey); + CompletableFuture.runAsync(this::loadKey, executor).whenCompleteAsync(this::loadedKey, Platform::runLater); } private void loadKey() { assert !Platform.isFxApplicationThread(); - assert unverifiedVaultConfig.isPresent(); - try (var masterkey = keyLoadingStrategy.loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) { - var unverifiedCfg = unverifiedVaultConfig.get(); + assert unverifiedVaultConfig.get() != null; + try { + keyLoadingStrategy.use(this::verifyVaultConfig); + } catch (VaultConfigLoadException | UnlockCancelledException e) { + throw new LoadingFailedException(e); + } + } + + private void verifyVaultConfig(KeyLoadingStrategy keyLoadingStrategy) throws VaultConfigLoadException { + var unverifiedCfg = unverifiedVaultConfig.get(); + try (var masterkey = keyLoadingStrategy.loadKey(unverifiedCfg.getKeyId())) { var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion()); vaultConfigRef.set(verifiedCfg); var old = masterkeyRef.getAndSet(masterkey.clone()); if (old != null) { old.destroy(); } - Platform.runLater(this::loadedKey); - } catch (MasterkeyLoadingFailedException e) { - if (keyLoadingStrategy.recoverFromException(e)) { - // retry - loadKey(); - } else { - Platform.runLater(() -> loadingKeyFailed(e)); - } - } catch (VaultKeyInvalidException e) { - Platform.runLater(() -> loadingKeyFailed(e)); - } catch (VaultConfigLoadException e) { - Platform.runLater(() -> loadingKeyFailed(e)); } } - private void loadedKey() { - LOG.debug("Loaded valid key"); - window.setScene(checkScene.get()); + private void loadedKey(Void unused, Throwable exception) { + assert Platform.isFxApplicationThread(); + if (exception instanceof LoadingFailedException) { + loadingKeyFailed(exception.getCause()); + } else if (exception != null) { + loadingKeyFailed(exception); + } else { + LOG.debug("Loaded valid key"); + unlockWindow.close(); + window.setScene(checkScene.get()); + } } - private void loadingKeyFailed(Exception e) { + private void loadingKeyFailed(Throwable e) { if (e instanceof UnlockCancelledException) { // ok } else if (e instanceof VaultKeyInvalidException) { @@ -120,8 +116,12 @@ public class StartController implements FxController { } } - public boolean isInvalidConfig() { - return unverifiedVaultConfig.isEmpty(); - } + /* internal types */ + private static class LoadingFailedException extends CompletionException { + + LoadingFailedException(Throwable cause) { + super(cause); + } + } } diff --git a/src/main/java/org/cryptomator/ui/health/StartFailController.java b/src/main/java/org/cryptomator/ui/health/StartFailController.java new file mode 100644 index 000000000..826766026 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/StartFailController.java @@ -0,0 +1,79 @@ +package org.cryptomator.ui.health; + +import com.google.common.base.Preconditions; +import org.cryptomator.cryptofs.VaultConfigLoadException; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.controls.FontAwesome5Icon; + +import javax.inject.Inject; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.fxml.FXML; +import javafx.scene.control.TitledPane; +import javafx.stage.Stage; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +// TODO reevaluate config loading, as soon as we have the new generic error screen +@HealthCheckScoped +public class StartFailController implements FxController { + + private final Stage window; + private final ObjectProperty loadError; + private final ObjectProperty moreInfoIcon; + + /* FXML */ + public TitledPane moreInfoPane; + + @Inject + public StartFailController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult) { + Preconditions.checkNotNull(configLoadResult.error()); + this.window = window; + this.loadError = new SimpleObjectProperty<>(configLoadResult.error()); + this.moreInfoIcon = new SimpleObjectProperty<>(FontAwesome5Icon.CARET_RIGHT); + } + + public void initialize() { + moreInfoPane.expandedProperty().addListener(this::setMoreInfoIcon); + } + + private void setMoreInfoIcon(ObservableValue observable, boolean wasExpanded, boolean willExpand) { + moreInfoIcon.set(willExpand ? FontAwesome5Icon.CARET_DOWN : FontAwesome5Icon.CARET_RIGHT); + } + + @FXML + public void close() { + window.close(); + } + + /* Getter & Setter */ + + public ObjectProperty moreInfoIconProperty() { + return moreInfoIcon; + } + + public FontAwesome5Icon getMoreInfoIcon() { + return moreInfoIcon.getValue(); + } + + public String getStackTrace() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + loadError.get().printStackTrace(new PrintStream(baos)); + return baos.toString(StandardCharsets.UTF_8); + } + + public String getLocalizedErrorMessage() { + return loadError.get().getLocalizedMessage(); + } + + public boolean isParseException() { + return loadError.get() instanceof VaultConfigLoadException; + } + + public boolean isIoException() { + return !isParseException(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java index ed8ca0540..614247ebc 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java @@ -3,6 +3,8 @@ package org.cryptomator.ui.keyloading; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.net.URI; @@ -12,6 +14,8 @@ import java.net.URI; @FunctionalInterface public interface KeyLoadingStrategy extends MasterkeyLoader { + Logger LOG = LoggerFactory.getLogger(KeyLoadingStrategy.class); + /** * Loads a master key. This might be a long-running operation, as it may require user input or expensive computations. *

@@ -35,7 +39,7 @@ public interface KeyLoadingStrategy extends MasterkeyLoader { } /** - * Release any ressources or do follow-up tasks after loading a key. + * Release any resources or do follow-up tasks after loading a key. * * @param unlockedSuccessfully true if successfully unlocked a vault with the loaded key * @implNote This method might be invoked multiple times, depending on whether multiple attempts to load a key are started. @@ -47,7 +51,7 @@ public interface KeyLoadingStrategy extends MasterkeyLoader { /** * A key loading strategy that will always fail by throwing a {@link MasterkeyLoadingFailedException}. * - * @param exception The cause of the failure. If not alreay an {@link MasterkeyLoadingFailedException}, it will get wrapped. + * @param exception The cause of the failure. If not already an {@link MasterkeyLoadingFailedException}, it will get wrapped. * @return A new KeyLoadingStrategy that will always fail with an {@link MasterkeyLoadingFailedException}. */ static KeyLoadingStrategy failed(Exception exception) { @@ -60,4 +64,36 @@ public interface KeyLoadingStrategy extends MasterkeyLoader { }; } + /** + * Makes the given user apply this key loading strategy. If the user fails with a {@link MasterkeyLoadingFailedException}, + * an attempt is made to {@link #recoverFromException(MasterkeyLoadingFailedException) recover} from it. Any other exception will be rethrown. + * + * @param user Some method using this strategy. May be invoked multiple times in case of recoverable {@link MasterkeyLoadingFailedException}s + * @param Optional exception type thrown by user + * @throws MasterkeyLoadingFailedException If a non-recoverable exception is thrown by user + * @throws E Exception thrown by user and rethrown by this method + */ + default void use(KeyLoadingStrategyUser user) throws MasterkeyLoadingFailedException, E { + boolean success = false; + try { + user.use(this); + } catch (MasterkeyLoadingFailedException e) { + if (recoverFromException(e)) { + LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName()); + use(user); + } else { + throw e; + } + } finally { + cleanup(success); + } + } + + @FunctionalInterface + interface KeyLoadingStrategyUser { + + void use(KeyLoadingStrategy strategy) throws MasterkeyLoadingFailedException, E; + + } + } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java index f8fbdd720..39db1cc04 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -32,7 +32,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { public static final String SCHEME = "masterkeyfile"; private final Vault vault; - private final MasterkeyFileAccess masterkeyFileAcccess; + private final MasterkeyFileAccess masterkeyFileAccess; private final Stage window; private final Lazy passphraseEntryScene; private final Lazy selectMasterkeyFileScene; @@ -45,9 +45,9 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { private boolean wrongPassword; @Inject - public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAcccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, UserInteractionLock passwordEntryLock, UserInteractionLock masterkeyFileProvisionLock, AtomicReference password, AtomicReference filePath, MasterkeyFileLoadingFinisher finisher) { + public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, UserInteractionLock passwordEntryLock, UserInteractionLock masterkeyFileProvisionLock, AtomicReference password, AtomicReference filePath, MasterkeyFileLoadingFinisher finisher) { this.vault = vault; - this.masterkeyFileAcccess = masterkeyFileAcccess; + this.masterkeyFileAccess = masterkeyFileAccess; this.window = window; this.passphraseEntryScene = passphraseEntryScene; this.selectMasterkeyFileScene = selectMasterkeyFileScene; @@ -68,7 +68,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { filePath = getAlternateMasterkeyFilePath(); } CharSequence passphrase = getPassphrase(); - return masterkeyFileAcccess.load(filePath, passphrase); + return masterkeyFileAccess.load(filePath, passphrase); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new UnlockCancelledException("Unlock interrupted", e); diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java index d4356be85..f6ce79e51 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java @@ -5,6 +5,7 @@ import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.common.WeakBindings; +import org.cryptomator.ui.controls.FontAwesome5IconView; import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; import org.cryptomator.ui.keyloading.KeyLoading; @@ -59,8 +60,10 @@ public class PassphraseEntryController implements FxController { private final BooleanProperty unlockButtonDisabled; private final StringBinding vaultName; + /* FXML */ public NiceSecurePasswordField passwordField; public CheckBox savePasswordCheckbox; + public FontAwesome5IconView unlockInProgressView; public ImageView face; public ImageView leftArm; public ImageView rightArm; diff --git a/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java b/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java index 49ef79e83..f4e5bb790 100644 --- a/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java +++ b/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java @@ -64,7 +64,7 @@ class AppLaunchEventHandler { } } - // TODO dedup MainWindowController... + // TODO deduplicate MainWindowController... private void addOrRevealVault(Path potentialVaultPath) { assert Platform.isFxApplicationThread(); try { diff --git a/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java b/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java index 75af409f1..66b75840b 100644 --- a/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java +++ b/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java @@ -19,6 +19,7 @@ import java.awt.Desktop; import java.awt.EventQueue; import java.awt.desktop.AboutEvent; import java.awt.desktop.QuitResponse; +import java.awt.desktop.QuitStrategy; import java.util.EnumSet; import java.util.EventObject; import java.util.Set; @@ -61,6 +62,11 @@ public class AppLifecycleListener { Desktop.getDesktop().setQuitHandler(this::handleQuitRequest); } + // set quit strategy (cmd+q would call `System.exit(0)` otherwise) + if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_STRATEGY)) { + Desktop.getDesktop().setQuitStrategy(QuitStrategy.CLOSE_ALL_WINDOWS); + } + shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults); } @@ -71,7 +77,7 @@ public class AppLifecycleListener { handleQuitRequest(null, new QuitResponse() { @Override public void performQuit() { - System.exit(0); + // no-op } @Override @@ -96,7 +102,7 @@ public class AppLifecycleListener { public void performQuit() { Platform.exit(); // will be no-op, if JavaFX never started. shutdownLatch.countDown(); // main thread is waiting for this latch - EventQueue.invokeLater(originalQuitResponse::performQuit); // this will eventually call System.exit(0) + originalQuitResponse.performQuit(); } @Override diff --git a/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java b/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java index 87fd486f2..73b4844b9 100644 --- a/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java +++ b/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java @@ -22,7 +22,7 @@ import javafx.stage.Window; /** * The sequence of actions performed and checked during lock of a vault. *

- * This class implements the Task interface, sucht that it can run in the background with some possible forground operations/requests to the ui, without blocking the main app. + * This class implements the Task interface, sucht that it can run in the background with some possible foreground operations/requests to the ui, without blocking the main app. * If the task state is *

  • succeeded, the vault was successfully locked;
  • *
  • canceled, the lock was canceled;
  • diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java index ea9311c88..ef050e799 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java @@ -129,7 +129,7 @@ public class MainWindowTitleController implements FxController { } public boolean isShowMinimizeButton() { - // always show the minimize button if no tray icon is present OR it is explicitily enabled + // always show the minimize button if no tray icon is present OR it is explicitly enabled return !trayMenuInitialized || settings.showMinimizeButton().get(); } } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java index 7b5c4c7c9..87a419a94 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java @@ -1,10 +1,14 @@ package org.cryptomator.ui.mainwindow; import com.tobiasdiez.easybind.EasyBind; +import com.tobiasdiez.easybind.Subscription; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.ui.common.Animations; +import org.cryptomator.ui.common.AutoAnimator; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.controls.FontAwesome5Icon; +import org.cryptomator.ui.controls.FontAwesome5IconView; import org.cryptomator.ui.fxapp.FxApplication; import javax.inject.Inject; @@ -22,6 +26,12 @@ public class VaultDetailController implements FxController { private final Binding glyph; private final BooleanBinding anyVaultSelected; + private AutoAnimator spinAnimation; + + /* FXML */ + public FontAwesome5IconView vaultStateView; + + @Inject VaultDetailController(ObjectProperty vault, FxApplication application) { this.vault = vault; @@ -32,6 +42,13 @@ public class VaultDetailController implements FxController { this.anyVaultSelected = vault.isNotNull(); } + public void initialize() { + this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) // + .onCondition(EasyBind.select(vault).selectObject(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals)) // + .afterStop(() -> vaultStateView.setRotate(0)) // + .build(); + } + // TODO deduplicate w/ VaultListCellController private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) { if (state != null) { diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java index f18369e7a..ef1ef1973 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java @@ -3,8 +3,11 @@ package org.cryptomator.ui.mainwindow; import com.tobiasdiez.easybind.EasyBind; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.ui.common.Animations; +import org.cryptomator.ui.common.AutoAnimator; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.controls.FontAwesome5Icon; +import org.cryptomator.ui.controls.FontAwesome5IconView; import javax.inject.Inject; import javafx.beans.binding.Binding; @@ -17,6 +20,11 @@ public class VaultListCellController implements FxController { private final ObjectProperty vault = new SimpleObjectProperty<>(); private final Binding glyph; + private AutoAnimator spinAnimation; + + /* FXML */ + public FontAwesome5IconView vaultStateView; + @Inject VaultListCellController() { this.glyph = EasyBind.select(vault) // @@ -24,6 +32,13 @@ public class VaultListCellController implements FxController { .map(this::getGlyphForVaultState); } + public void initialize() { + this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) // + .onCondition(EasyBind.select(vault).selectObject(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals)) // + .afterStop(() -> vaultStateView.setRotate(0)) // + .build(); + } + // TODO deduplicate w/ VaultDetailController private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) { if (state != null) { @@ -59,4 +74,5 @@ public class VaultListCellController implements FxController { public void setVault(Vault value) { vault.set(value); } + } diff --git a/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java b/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java index a9b0085d8..503814b25 100644 --- a/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java +++ b/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java @@ -66,7 +66,10 @@ public class MigrationRunController implements FxController { private final Lazy capabilityErrorScene; private final BooleanProperty migrationButtonDisabled; private final DoubleProperty migrationProgress; + private volatile double volatileMigrationProgress = -1.0; + + /* FXML */ public NiceSecurePasswordField passwordField; @Inject diff --git a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java index 64d71a8b7..fafea3f2f 100644 --- a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java @@ -2,7 +2,6 @@ package org.cryptomator.ui.preferences; import org.cryptomator.common.Environment; import org.cryptomator.common.LicenseHolder; -import org.cryptomator.common.settings.KeychainBackend; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.UiTheme; import org.cryptomator.integrations.autostart.AutoStartProvider; @@ -16,6 +15,7 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.application.Application; +import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; @@ -27,11 +27,9 @@ import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import javafx.stage.Stage; import javafx.util.StringConverter; -import java.util.Arrays; import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; -import java.util.stream.Collectors; @PreferencesScoped public class GeneralPreferencesController implements FxController { @@ -51,7 +49,7 @@ public class GeneralPreferencesController implements FxController { private final Set keychainAccessProviders; private final ErrorComponent.Builder errorComponent; public ChoiceBox themeChoiceBox; - public ChoiceBox keychainBackendChoiceBox; + public ChoiceBox keychainBackendChoiceBox; public CheckBox showMinimizeButtonCheckbox; public CheckBox showTrayIconCheckbox; public CheckBox startHiddenCheckbox; @@ -101,15 +99,13 @@ public class GeneralPreferencesController implements FxController { nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT); nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation); - keychainBackendChoiceBox.getItems().addAll(getAvailableBackends()); - keychainBackendChoiceBox.setConverter(new KeychainBackendConverter(resourceBundle)); - keychainBackendChoiceBox.valueProperty().bindBidirectional(settings.keychainBackend()); + var keychainSettingsConverter = new KeychainProviderClassNameConverter(keychainAccessProviders); + keychainBackendChoiceBox.getItems().addAll(keychainAccessProviders); + keychainBackendChoiceBox.setValue(keychainSettingsConverter.fromString(settings.keychainProvider().get())); + keychainBackendChoiceBox.setConverter(new KeychainProviderDisplayNameConverter()); + Bindings.bindBidirectional(settings.keychainProvider(), keychainBackendChoiceBox.valueProperty(), keychainSettingsConverter); } - private KeychainBackend[] getAvailableBackends() { - var namesOfAvailableProviders = keychainAccessProviders.stream().map(KeychainAccessProvider::getClass).map(Class::getName).collect(Collectors.toUnmodifiableSet()); - return Arrays.stream(KeychainBackend.values()).filter(value -> namesOfAvailableProviders.contains(value.getProviderClass())).toArray(KeychainBackend[]::new); - } public boolean isTrayMenuInitialized() { return trayMenuInitialized; @@ -188,23 +184,48 @@ public class GeneralPreferencesController implements FxController { } - private static class KeychainBackendConverter extends StringConverter { + private class KeychainProviderDisplayNameConverter extends StringConverter { - private final ResourceBundle resourceBundle; - - KeychainBackendConverter(ResourceBundle resourceBundle) { - this.resourceBundle = resourceBundle; + @Override + public String toString(KeychainAccessProvider provider) { + if (provider == null) { + return null; + } else { + return provider.displayName(); + } } @Override - public String toString(KeychainBackend impl) { - return resourceBundle.getString("preferences.general.keychainBackend." + impl.getProviderClass()); - } - - @Override - public KeychainBackend fromString(String string) { + public KeychainAccessProvider fromString(String string) { throw new UnsupportedOperationException(); } } + + private static class KeychainProviderClassNameConverter extends StringConverter { + + private final Set keychainAccessProviders; + + public KeychainProviderClassNameConverter(Set keychainAccessProviders) { + this.keychainAccessProviders = keychainAccessProviders; + } + + @Override + public String toString(KeychainAccessProvider provider) { + if (provider == null) { + return null; + } else { + return provider.getClass().getName(); + } + } + + @Override + public KeychainAccessProvider fromString(String string) { + if (string == null) { + return null; + } else { + return keychainAccessProviders.stream().filter(provider -> provider.getClass().getName().equals(string)).findAny().orElse(null); + } + } + } } diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java index bb0f43edd..1714d00a6 100644 --- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java @@ -26,6 +26,8 @@ public class UpdatesPreferencesController implements FxController { private final ReadOnlyStringProperty latestVersion; private final ReadOnlyStringProperty currentVersion; private final BooleanBinding updateAvailable; + + /* FXML */ public CheckBox checkForUpdatesCheckbox; @Inject diff --git a/src/main/java/org/cryptomator/ui/quit/QuitController.java b/src/main/java/org/cryptomator/ui/quit/QuitController.java index 12ddbbca3..207ae42c8 100644 --- a/src/main/java/org/cryptomator/ui/quit/QuitController.java +++ b/src/main/java/org/cryptomator/ui/quit/QuitController.java @@ -30,6 +30,8 @@ public class QuitController implements FxController { private final ExecutorService executorService; private final VaultService vaultService; private final AtomicReference quitResponse = new AtomicReference<>(); + + /* FXML */ public Button lockAndQuitButton; @Inject diff --git a/src/main/java/org/cryptomator/ui/recoverykey/AutoCompleter.java b/src/main/java/org/cryptomator/ui/recoverykey/AutoCompleter.java index 5c747261b..daac7f459 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/AutoCompleter.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/AutoCompleter.java @@ -26,7 +26,7 @@ public class AutoCompleter { if (Strings.isNullOrEmpty(prefix)) { return Optional.empty(); } - int potentialMatchIdx = findIndexOfLexicographicallyPreceeding(0, dictionary.size(), prefix); + int potentialMatchIdx = findIndexOfLexicographicallyPreceding(0, dictionary.size(), prefix); if (potentialMatchIdx < dictionary.size()) { String potentialMatch = dictionary.get(potentialMatchIdx); return potentialMatch.startsWith(prefix) ? Optional.of(potentialMatch) : Optional.empty(); @@ -48,21 +48,21 @@ public class AutoCompleter { * @param prefix * @return index between [0, dictLen], i.e. index can exceed the upper bounds of {@link #dictionary}. */ - private int findIndexOfLexicographicallyPreceeding(int begin, int end, String prefix) { + private int findIndexOfLexicographicallyPreceding(int begin, int end, String prefix) { if (begin >= end) { return begin; // this is usually where a binary search ends "unsuccessful" } int mid = (begin + end) / 2; String word = dictionary.get(mid); - if (prefix.compareTo(word) <= 0) { // prefix preceeds or matches word + if (prefix.compareTo(word) <= 0) { // prefix precedes or matches word // proceed in left half assert mid < end; - return findIndexOfLexicographicallyPreceeding(0, mid, prefix); + return findIndexOfLexicographicallyPreceding(0, mid, prefix); } else { // proceed in right half assert mid >= begin; - return findIndexOfLexicographicallyPreceeding(mid + 1, end, prefix); + return findIndexOfLexicographicallyPreceding(mid + 1, end, prefix); } } diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java index a2319ba3c..705991287 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java @@ -86,11 +86,11 @@ public class RecoveryKeyResetPasswordController implements FxController { /* Getter/Setter */ public ReadOnlyBooleanProperty validPasswordProperty() { - return newPasswordController.passwordsMatchAndSufficientProperty(); + return newPasswordController.goodPasswordProperty(); } public boolean isValidPassword() { - return newPasswordController.passwordsMatchAndSufficientProperty().get(); + return newPasswordController.isGoodPassword(); } } diff --git a/src/main/java/org/cryptomator/ui/recoverykey/WordEncoder.java b/src/main/java/org/cryptomator/ui/recoverykey/WordEncoder.java index ba4753879..7d31d660c 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/WordEncoder.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/WordEncoder.java @@ -58,7 +58,7 @@ class WordEncoder { * @throws IllegalArgumentException If input is not a multiple of three bytes */ public String encodePadded(byte[] input) { - Preconditions.checkArgument(input.length % 3 == 0, "input needs to be padded to a multipe of three"); + Preconditions.checkArgument(input.length % 3 == 0, "input needs to be padded to a multiple of three"); StringBuilder sb = new StringBuilder(); for (int i = 0; i < input.length; i += 3) { byte b1 = input[i]; @@ -85,12 +85,12 @@ class WordEncoder { * @throws IllegalArgumentException If the encoded string doesn't consist of a multiple of two words or one of the words is unknown to this encoder. */ public byte[] decode(String encoded) { - List splitted = Splitter.on(DELIMITER).omitEmptyStrings().splitToList(Strings.nullToEmpty(encoded)); - Preconditions.checkArgument(splitted.size() % 2 == 0, "%s needs to be a multiple of two words", encoded); - byte[] result = new byte[splitted.size() / 2 * 3]; - for (int i = 0; i < splitted.size(); i += 2) { - String w1 = splitted.get(i); - String w2 = splitted.get(i + 1); + List split = Splitter.on(DELIMITER).omitEmptyStrings().splitToList(Strings.nullToEmpty(encoded)); + Preconditions.checkArgument(split.size() % 2 == 0, "%s needs to be a multiple of two words", encoded); + byte[] result = new byte[split.size() / 2 * 3]; + for (int i = 0; i < split.size(); i += 2) { + String w1 = split.get(i); + String w2 = split.get(i + 1); int firstWordIndex = indices.getOrDefault(w1, -1); int secondWordIndex = indices.getOrDefault(w2, -1); Preconditions.checkArgument(firstWordIndex != -1, "%s not in dictionary", w1); diff --git a/src/main/java/org/cryptomator/ui/stats/VaultStatisticsController.java b/src/main/java/org/cryptomator/ui/stats/VaultStatisticsController.java index f8937e90f..a3c430946 100644 --- a/src/main/java/org/cryptomator/ui/stats/VaultStatisticsController.java +++ b/src/main/java/org/cryptomator/ui/stats/VaultStatisticsController.java @@ -65,8 +65,8 @@ public class VaultStatisticsController implements FxController { this.cacheHitRate = WeakBindings.bindDouble(stats.cacheHitRateProperty()); this.cacheHitDegrees = cacheHitRate.multiply(-270); this.cacheHitPercentage = cacheHitRate.multiply(100); - this.totalBytesRead = WeakBindings.bindLong(stats.toalBytesReadProperty()); - this.totalBytesWritten = WeakBindings.bindLong(stats.toalBytesWrittenProperty()); + this.totalBytesRead = WeakBindings.bindLong(stats.totalBytesReadProperty()); + this.totalBytesWritten = WeakBindings.bindLong(stats.totalBytesWrittenProperty()); this.totalBytesDecrypted = WeakBindings.bindLong(stats.totalBytesDecryptedProperty()); this.totalBytesEncrypted = WeakBindings.bindLong(stats.totalBytesEncryptedProperty()); this.filesRead = WeakBindings.bindLong(stats.filesRead()); @@ -102,7 +102,7 @@ public class VaultStatisticsController implements FxController { this.decryptedBytesRead = readData; this.encryptedBytesWrite = writeData; - // initialize data once and change value of datapoints later: + // initialize data once and change value of data points later: for (int i = 0; i < IO_SAMPLING_STEPS; i++) { decryptedBytesRead.getData().add(new Data<>(i, 0)); encryptedBytesWrite.getData().add(new Data<>(i, 0)); diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java b/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java index ee3ffc5ae..835654242 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java @@ -32,6 +32,7 @@ public class UnlockSuccessController implements FxController { private final ObjectProperty revealButtonState; private final BooleanProperty revealButtonDisabled; + /* FXML */ public CheckBox rememberChoiceCheckbox; @Inject diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index 36c3eacf9..073258d80 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -1,5 +1,6 @@ package org.cryptomator.ui.unlock; +import com.google.common.base.Throwables; import dagger.Lazy; import org.cryptomator.common.mountpoint.InvalidMountPointException; import org.cryptomator.common.vaults.MountPointRequirement; @@ -7,12 +8,10 @@ import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.Volume.VolumeException; import org.cryptomator.cryptolib.api.CryptoException; -import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.VaultService; -import org.cryptomator.ui.keyloading.KeyLoadingComponent; import org.cryptomator.ui.keyloading.KeyLoadingStrategy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,19 +67,14 @@ public class UnlockWorkflow extends Task { } private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException { - boolean success = false; try { - vault.unlock(keyLoadingStrategy); - success = true; - } catch (MasterkeyLoadingFailedException e) { - if (keyLoadingStrategy.recoverFromException(e)) { - LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName()); - attemptUnlock(); - } else { - throw e; - } - } finally { - keyLoadingStrategy.cleanup(success); + keyLoadingStrategy.use(vault::unlock); + } catch (Exception e) { + Throwables.propagateIfPossible(e, IOException.class); + Throwables.propagateIfPossible(e, VolumeException.class); + Throwables.propagateIfPossible(e, InvalidMountPointException.class); + Throwables.propagateIfPossible(e, CryptoException.class); + throw new IllegalStateException("unexpected exception type", e); } } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/AutoLockVaultOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/AutoLockVaultOptionsController.java deleted file mode 100644 index 1bb74690f..000000000 --- a/src/main/java/org/cryptomator/ui/vaultoptions/AutoLockVaultOptionsController.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.cryptomator.ui.vaultoptions; - -import org.cryptomator.common.vaults.Vault; -import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.controls.NumericTextField; - -import javax.inject.Inject; -import javafx.beans.binding.Bindings; -import javafx.fxml.FXML; -import javafx.scene.control.CheckBox; -import javafx.util.StringConverter; - -@VaultOptionsScoped -public class AutoLockVaultOptionsController implements FxController { - - private final Vault vault; - - public CheckBox lockAfterTimeCheckbox; - public NumericTextField lockTimeInMinutesTextField; - - @Inject - AutoLockVaultOptionsController(@VaultOptionsWindow Vault vault) { - this.vault = vault; - } - - @FXML - public void initialize() { - lockAfterTimeCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().autoLockWhenIdle()); - Bindings.bindBidirectional(lockTimeInMinutesTextField.textProperty(), vault.getVaultSettings().autoLockIdleSeconds(), new IdleTimeSecondsConverter()); - } - - private static class IdleTimeSecondsConverter extends StringConverter { - - @Override - public String toString(Number seconds) { - int minutes = seconds.intValue() / 60; // int-truncate - return Integer.toString(minutes); - } - - @Override - public Number fromString(String string) { - try { - int minutes = Integer.valueOf(string); - return minutes * 60; - } catch (NumberFormatException e) { - return 0; - } - } - } - -} diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java index 79d3b53ad..a35b108d9 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java @@ -3,10 +3,13 @@ package org.cryptomator.ui.vaultoptions; import org.cryptomator.common.settings.WhenUnlocked; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.controls.NumericTextField; import org.cryptomator.ui.health.HealthCheckComponent; import javax.inject.Inject; import javafx.beans.Observable; +import javafx.beans.binding.Bindings; +import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; @@ -29,6 +32,8 @@ public class GeneralVaultOptionsController implements FxController { public TextField vaultName; public CheckBox unlockOnStartupCheckbox; public ChoiceBox actionAfterUnlockChoiceBox; + public CheckBox lockAfterTimeCheckbox; + public NumericTextField lockTimeInMinutesTextField; @Inject GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow, ResourceBundle resourceBundle) { @@ -47,6 +52,8 @@ public class GeneralVaultOptionsController implements FxController { actionAfterUnlockChoiceBox.getItems().addAll(WhenUnlocked.values()); actionAfterUnlockChoiceBox.valueProperty().bindBidirectional(vault.getVaultSettings().actionAfterUnlock()); actionAfterUnlockChoiceBox.setConverter(new WhenUnlockedConverter(resourceBundle)); + lockAfterTimeCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().autoLockWhenIdle()); + Bindings.bindBidirectional(lockTimeInMinutesTextField.textProperty(), vault.getVaultSettings().autoLockIdleSeconds(), new IdleTimeSecondsConverter()); } private void trimVaultNameOnFocusLoss(Observable observable, Boolean wasFocussed, Boolean isFocussed) { @@ -64,12 +71,6 @@ public class GeneralVaultOptionsController implements FxController { } } - @FXML - public void showHealthCheck() { - healthCheckWindow.vault(vault).build().showHealthCheckWindow(); - } - - private static class WhenUnlockedConverter extends StringConverter { private final ResourceBundle resourceBundle; @@ -89,4 +90,26 @@ public class GeneralVaultOptionsController implements FxController { } } + private static class IdleTimeSecondsConverter extends StringConverter { + + @Override + public String toString(Number seconds) { + int minutes = seconds.intValue() / 60; // int-truncate + return Integer.toString(minutes); + } + + @Override + public Number fromString(String string) { + try { + int minutes = Integer.valueOf(string); + return minutes * 60; + } catch (NumberFormatException e) { + return 0; + } + } + } + + public void startHealthCheck() { + healthCheckWindow.vault(vault).owner(window).build().showHealthCheckWindow(); + } } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java b/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java index 03a4922d4..3fc738fb0 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java @@ -21,9 +21,4 @@ public enum SelectedVaultOptionsTab { */ KEY, - /** - * Show Auto-Lock tab - * - */ - AUTOLOCK, } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java index 20dac7594..15879e316 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java @@ -23,7 +23,6 @@ public class VaultOptionsController implements FxController { public Tab generalTab; public Tab mountTab; public Tab keyTab; - public Tab autoLockTab; @Inject VaultOptionsController(@VaultOptionsWindow Stage window, ObjectProperty selectedTabProperty) { @@ -48,7 +47,6 @@ public class VaultOptionsController implements FxController { case ANY, GENERAL -> generalTab; case MOUNT -> mountTab; case KEY -> keyTab; - case AUTOLOCK -> autoLockTab; }; } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java index 89cef234b..cb6c109b9 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java @@ -83,10 +83,4 @@ abstract class VaultOptionsModule { @IntoMap @FxControllerKey(MasterkeyOptionsController.class) abstract FxController bindMasterkeyOptionsController(MasterkeyOptionsController controller); - - @Binds - @IntoMap - @FxControllerKey(AutoLockVaultOptionsController.class) - abstract FxController bindAutoLockVaultOptionsController(AutoLockVaultOptionsController controller); - } diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css index 8d20ad9be..a00e46845 100644 --- a/src/main/resources/css/dark_theme.css +++ b/src/main/resources/css/dark_theme.css @@ -127,23 +127,38 @@ -fx-fill: TEXT_FILL; } -.glyph-icon-primary { +.glyph-icon-primary, +.glyph-icon.glyph-icon-primary, +.list-cell .glyph-icon.glyph-icon-primary, +.list-cell:selected .glyph-icon.glyph-icon-primary { -fx-fill: PRIMARY; } -.glyph-icon-muted { +.glyph-icon-muted, +.glyph-icon.glyph-icon-muted, +.list-cell .glyph-icon.glyph-icon-muted, +.list-cell:selected .glyph-icon.glyph-icon-muted { -fx-fill: TEXT_FILL_MUTED; } -.glyph-icon-white { +.glyph-icon-white, +.glyph-icon.glyph-icon-white, +.list-cell .glyph-icon.glyph-icon-white, +.list-cell:selected .glyph-icon.glyph-icon-white { -fx-fill: white; } -.glyph-icon-red { +.glyph-icon-red, +.glyph-icon.glyph-icon-red, +.list-cell .glyph-icon.glyph-icon-red, +.list-cell:selected .glyph-icon.glyph-icon-red { -fx-fill: RED_5; } -.glyph-icon-orange { +.glyph-icon-orange, +.glyph-icon.glyph-icon-orange, +.list-cell .glyph-icon.glyph-icon-orange, +.list-cell:selected .glyph-icon.glyph-icon-orange { -fx-fill: ORANGE_5; } @@ -568,6 +583,10 @@ -fx-fill: white; } +.button:default:disabled .glyph-icon { + -fx-fill: TEXT_FILL_MUTED; +} + .button:default .label { -fx-text-fill: white; } @@ -788,75 +807,6 @@ -fx-scale-shape: false; } -/******************************************************************************* - * * - * ProgressIndicator * - * Derived from aquafx-project.com, (C) Claudine Zillmann, see NOTICE.md * - * * - ******************************************************************************/ - -.progress-indicator { - -fx-indeterminate-segment-count: 12; - -fx-spin-enabled: true; -} - -.progress-indicator:indeterminate > .spinner { - -fx-padding: 0.083333em; -} - -.progress-indicator:indeterminate .segment { - -fx-background-color: PROGRESS_INDICATOR_BEGIN, PROGRESS_INDICATOR_END; - -fx-background-insets: 0, 0.5; -} - -.progress-indicator:indeterminate .segment0 { - -fx-shape: "m 12.007729,4.9541827 c -0.49762,0.7596865 0.893181,1.6216808 1.327833,0.7666252 L 15.456199,2.0477574 C 15.942094,1.2061627 14.61426,0.43953765 14.128365,1.2811324 z"; -} - -.progress-indicator:indeterminate .segment1 { - -fx-shape: "m 9.2224559,4.62535 c -0.051108,0.9067177 1.5843581,0.957826 1.5332501,0 l 0,-4.24127319 c 0,-0.9717899 -1.5332501,-0.9717899 -1.5332501,0 z"; -} - -.progress-indicator:indeterminate .segment2 { - -fx-shape: "M 8.0465401,4.9030617 C 8.5441601,5.6627485 7.1533584,6.5247425 6.7187068,5.6696872 L 4.5980702,1.9966363 C 4.1121752,1.1550418 5.4400085,0.38841683 5.9259035,1.2300114 z"; -} - -.progress-indicator:indeterminate .segment3 { - -fx-shape: "M 5.7330066,6.5305598 C 6.5579512,6.9103162 5.8366865,8.3790371 5.0144939,7.8850315 L 1.2677551,5.8974832 C 0.409277,5.4420823 1.1277888,4.0876101 1.9862674,4.5430105 z"; -} - -.progress-indicator:indeterminate .segment4 { - -fx-shape: "m 0.42171041,9.2083842 c -0.90671825,-0.051108 -0.95782608,1.5843588 0,1.5332498 l 4.24127319,0 c 0.9717899,0 0.9717899,-1.5332498 0,-1.5332498 z"; -} - -.progress-indicator:indeterminate .segment5 { - -fx-shape: "M 5.7330066,13.443113 C 6.5579512,13.063356 5.8366865,11.594635 5.0144939,12.088641 L 1.2677551,14.076189 C 0.409277,14.53159 1.1277888,15.886062 1.9862674,15.430662 z"; -} - -.progress-indicator:indeterminate .segment6 { - -fx-shape: "M 8.0465401,15.070611 C 8.5441601,14.310924 7.1533584,13.44893 6.7187068,14.303985 l -2.1206366,3.673051 c -0.485895,0.841595 0.8419383,1.60822 1.3278333,0.766625 z"; -} - -.progress-indicator:indeterminate .segment7 { - -fx-shape: "m 9.2224559,19.539943 c -0.051108,0.906718 1.5843581,0.957826 1.5332501,0 l 0,-4.241273 c 0,-0.97179 -1.5332501,-0.97179 -1.5332501,0 z"; -} - -.progress-indicator:indeterminate .segment8 { - -fx-shape: "m 12.10997,15.070611 c -0.49762,-0.759687 0.893182,-1.621681 1.327834,-0.766626 l 2.120636,3.673051 c 0.485895,0.841595 -0.841938,1.60822 -1.327833,0.766625 z"; -} - -.progress-indicator:indeterminate .segment9 { - -fx-shape: "m 14.423504,13.443113 c -0.824945,-0.379757 -0.10368,-1.848478 0.718512,-1.354472 l 3.746739,1.987548 c 0.858478,0.455401 0.139967,1.809873 -0.718512,1.354473 z"; -} - -.progress-indicator:indeterminate .segment10 { - -fx-shape: "m 15.372451,9.2445322 c -0.906719,-0.051108 -0.957826,1.5843588 0,1.5332498 l 4.241273,0 c 0.97179,0 0.97179,-1.5332498 0,-1.5332498 z"; -} - -.progress-indicator:indeterminate .segment11 { - -fx-shape: "m 14.321262,6.5816808 c -0.824944,0.3797564 -0.10368,1.8484772 0.718513,1.3544717 L 18.786514,5.9486042 C 19.644992,5.4932031 18.92648,4.1387308 18.068001,4.5941315 z"; -} - /******************************************************************************* * * * ProgressBar * diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css index b0ba8ac8c..09c743705 100644 --- a/src/main/resources/css/light_theme.css +++ b/src/main/resources/css/light_theme.css @@ -127,23 +127,38 @@ -fx-fill: TEXT_FILL; } -.glyph-icon-primary { +.glyph-icon-primary, +.glyph-icon.glyph-icon-primary, +.list-cell .glyph-icon.glyph-icon-primary, +.list-cell:selected .glyph-icon.glyph-icon-primary { -fx-fill: PRIMARY; } -.glyph-icon-muted { +.glyph-icon-muted, +.glyph-icon.glyph-icon-muted, +.list-cell .glyph-icon.glyph-icon-muted, +.list-cell:selected .glyph-icon.glyph-icon-muted { -fx-fill: TEXT_FILL_MUTED; } -.glyph-icon-white { +.glyph-icon-white, +.glyph-icon.glyph-icon-white, +.list-cell .glyph-icon.glyph-icon-white, +.list-cell:selected .glyph-icon.glyph-icon-white { -fx-fill: white; } -.glyph-icon-red { +.glyph-icon-red, +.glyph-icon.glyph-icon-red, +.list-cell .glyph-icon.glyph-icon-red, +.list-cell:selected .glyph-icon.glyph-icon-red { -fx-fill: RED_5; } -.glyph-icon-orange { +.glyph-icon-orange, +.glyph-icon.glyph-icon-orange, +.list-cell .glyph-icon.glyph-icon-orange, +.list-cell:selected .glyph-icon.glyph-icon-orange { -fx-fill: ORANGE_5; } @@ -566,6 +581,10 @@ -fx-fill: white; } +.button:default:disabled .glyph-icon { + -fx-fill: TEXT_FILL_MUTED; +} + .button:default .label { -fx-text-fill: white; } @@ -786,75 +805,6 @@ -fx-scale-shape: false; } -/******************************************************************************* - * * - * ProgressIndicator * - * Derived from aquafx-project.com, (C) Claudine Zillmann, see NOTICE.md * - * * - ******************************************************************************/ - -.progress-indicator { - -fx-indeterminate-segment-count: 12; - -fx-spin-enabled: true; -} - -.progress-indicator:indeterminate > .spinner { - -fx-padding: 0.083333em; -} - -.progress-indicator:indeterminate .segment { - -fx-background-color: PROGRESS_INDICATOR_BEGIN, PROGRESS_INDICATOR_END; - -fx-background-insets: 0, 0.5; -} - -.progress-indicator:indeterminate .segment0 { - -fx-shape: "m 12.007729,4.9541827 c -0.49762,0.7596865 0.893181,1.6216808 1.327833,0.7666252 L 15.456199,2.0477574 C 15.942094,1.2061627 14.61426,0.43953765 14.128365,1.2811324 z"; -} - -.progress-indicator:indeterminate .segment1 { - -fx-shape: "m 9.2224559,4.62535 c -0.051108,0.9067177 1.5843581,0.957826 1.5332501,0 l 0,-4.24127319 c 0,-0.9717899 -1.5332501,-0.9717899 -1.5332501,0 z"; -} - -.progress-indicator:indeterminate .segment2 { - -fx-shape: "M 8.0465401,4.9030617 C 8.5441601,5.6627485 7.1533584,6.5247425 6.7187068,5.6696872 L 4.5980702,1.9966363 C 4.1121752,1.1550418 5.4400085,0.38841683 5.9259035,1.2300114 z"; -} - -.progress-indicator:indeterminate .segment3 { - -fx-shape: "M 5.7330066,6.5305598 C 6.5579512,6.9103162 5.8366865,8.3790371 5.0144939,7.8850315 L 1.2677551,5.8974832 C 0.409277,5.4420823 1.1277888,4.0876101 1.9862674,4.5430105 z"; -} - -.progress-indicator:indeterminate .segment4 { - -fx-shape: "m 0.42171041,9.2083842 c -0.90671825,-0.051108 -0.95782608,1.5843588 0,1.5332498 l 4.24127319,0 c 0.9717899,0 0.9717899,-1.5332498 0,-1.5332498 z"; -} - -.progress-indicator:indeterminate .segment5 { - -fx-shape: "M 5.7330066,13.443113 C 6.5579512,13.063356 5.8366865,11.594635 5.0144939,12.088641 L 1.2677551,14.076189 C 0.409277,14.53159 1.1277888,15.886062 1.9862674,15.430662 z"; -} - -.progress-indicator:indeterminate .segment6 { - -fx-shape: "M 8.0465401,15.070611 C 8.5441601,14.310924 7.1533584,13.44893 6.7187068,14.303985 l -2.1206366,3.673051 c -0.485895,0.841595 0.8419383,1.60822 1.3278333,0.766625 z"; -} - -.progress-indicator:indeterminate .segment7 { - -fx-shape: "m 9.2224559,19.539943 c -0.051108,0.906718 1.5843581,0.957826 1.5332501,0 l 0,-4.241273 c 0,-0.97179 -1.5332501,-0.97179 -1.5332501,0 z"; -} - -.progress-indicator:indeterminate .segment8 { - -fx-shape: "m 12.10997,15.070611 c -0.49762,-0.759687 0.893182,-1.621681 1.327834,-0.766626 l 2.120636,3.673051 c 0.485895,0.841595 -0.841938,1.60822 -1.327833,0.766625 z"; -} - -.progress-indicator:indeterminate .segment9 { - -fx-shape: "m 14.423504,13.443113 c -0.824945,-0.379757 -0.10368,-1.848478 0.718512,-1.354472 l 3.746739,1.987548 c 0.858478,0.455401 0.139967,1.809873 -0.718512,1.354473 z"; -} - -.progress-indicator:indeterminate .segment10 { - -fx-shape: "m 15.372451,9.2445322 c -0.906719,-0.051108 -0.957826,1.5843588 0,1.5332498 l 4.241273,0 c 0.97179,0 0.97179,-1.5332498 0,-1.5332498 z"; -} - -.progress-indicator:indeterminate .segment11 { - -fx-shape: "m 14.321262,6.5816808 c -0.824944,0.3797564 -0.10368,1.8484772 0.718513,1.3544717 L 18.786514,5.9486042 C 19.644992,5.4932031 18.92648,4.1387308 18.068001,4.5941315 z"; -} - /******************************************************************************* * * * ProgressBar * diff --git a/src/main/resources/fxml/addvault_new_password.fxml b/src/main/resources/fxml/addvault_new_password.fxml index 4b62f9b78..68f0200b5 100644 --- a/src/main/resources/fxml/addvault_new_password.fxml +++ b/src/main/resources/fxml/addvault_new_password.fxml @@ -4,11 +4,11 @@ - + - + @@ -41,7 +41,7 @@ diff --git a/src/main/resources/fxml/addvault_new_recoverykey.fxml b/src/main/resources/fxml/addvault_new_recoverykey.fxml index 956461fe9..b04677bc3 100644 --- a/src/main/resources/fxml/addvault_new_recoverykey.fxml +++ b/src/main/resources/fxml/addvault_new_recoverykey.fxml @@ -18,7 +18,7 @@ - + diff --git a/src/main/resources/fxml/addvault_welcome.fxml b/src/main/resources/fxml/addvault_welcome.fxml index 750a574a2..7d7244973 100644 --- a/src/main/resources/fxml/addvault_welcome.fxml +++ b/src/main/resources/fxml/addvault_welcome.fxml @@ -21,7 +21,7 @@ - + diff --git a/src/main/resources/fxml/changepassword.fxml b/src/main/resources/fxml/changepassword.fxml index 3f077df5d..eaab7b06a 100644 --- a/src/main/resources/fxml/changepassword.fxml +++ b/src/main/resources/fxml/changepassword.fxml @@ -25,7 +25,7 @@ - + diff --git a/src/main/resources/fxml/health_check_details.fxml b/src/main/resources/fxml/health_check_details.fxml index 51d9b22ae..65c6a409a 100644 --- a/src/main/resources/fxml/health_check_details.fxml +++ b/src/main/resources/fxml/health_check_details.fxml @@ -1,24 +1,29 @@ - + + - + - \ No newline at end of file diff --git a/src/main/resources/fxml/health_check_list.fxml b/src/main/resources/fxml/health_check_list.fxml index a3a9c2f93..1d8054ab6 100644 --- a/src/main/resources/fxml/health_check_list.fxml +++ b/src/main/resources/fxml/health_check_list.fxml @@ -3,17 +3,18 @@ + + - - @@ -23,23 +24,26 @@ - -