Compare commits

..
Author SHA1 Message Date
JaniruTEC 0dca0d6f1c Fixed #2270 2022-06-30 02:28:42 +02:00
274 changed files with 4468 additions and 9603 deletions
+34 -32
View File
@@ -10,38 +10,43 @@ on:
required: false
env:
JAVA_VERSION: 19
JAVA_VERSION: 17
jobs:
get-version:
uses: ./.github/workflows/get-version.yml
with:
version: ${{ github.event.inputs.version }}
build:
name: Build AppImage
runs-on: ubuntu-latest
needs: [get-version]
steps:
- uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
- uses: actions/checkout@v2
with:
distribution: 'zulu'
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
java-package: 'jdk+fx'
cache: 'maven'
- name: Ensure major jfx version in pom equals in jdk
shell: pwsh
- id: versions
name: Apply version information
run: |
$jfxPomVersion = (&mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout) -split "\."
$jfxJdkVersion = ((Get-Content -path "${env:JAVA_HOME}/lib/javafx.properties" | Where-Object {$_ -like 'javafx.version=*' }) -replace '.*=','') -split "\."
if ($jfxPomVersion[0] -ne $jfxJdkVersion[0]) {
Write-Error "Major part of JavaFX version in pom($($jfxPomVersion[0])) does not match the version in JDK($($jfxJdkVersion[0])) "
exit 1
}
- name: Set version
run : mvn versions:set -DnewVersion=${{ needs.get-version.outputs.semVerStr }}
if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR=${GITHUB_REF##*/}
mvn versions:set -DnewVersion=${SEM_VER_STR}
elif [[ "${{ github.event.inputs.version }}" =~ [0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR="${{ github.event.inputs.version }}"
mvn versions:set -DnewVersion=${SEM_VER_STR}
else
SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout`
fi
SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'`
REVCOUNT=`git rev-list --count HEAD`
echo "::set-output name=semVerStr::${SEM_VER_STR}"
echo "::set-output name=semVerNum::${SEM_VER_NUM}"
echo "::set-output name=revNum::${REVCOUNT}"
- name: Validate Version
uses: skymatic/semver-validation-action@v1
with:
version: ${{ steps.versions.outputs.semVerStr }}
- name: Run maven
run: mvn -B clean package -Pdependency-check,linux -DskipTests
- name: Patch target dir
@@ -55,7 +60,7 @@ jobs:
--verbose
--output runtime
--module-path "${JAVA_HOME}/jmods"
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--strip-native-commands
--no-header-files
--no-man-pages
@@ -64,8 +69,8 @@ jobs:
- name: Prepare additional launcher
run: envsubst '${SEMVER_STR} ${REVISION_NUM}' < dist/linux/launcher-gtk2.properties > launcher-gtk2.properties
env:
SEMVER_STR: ${{ needs.get-version.outputs.semVerStr }}
REVISION_NUM: ${{ needs.get-version.outputs.revNum }}
SEMVER_STR: ${{ steps.versions.outputs.semVerStr }}
REVISION_NUM: ${{ steps.versions.outputs.revNum }}
- name: Run jpackage
run: >
${JAVA_HOME}/bin/jpackage
@@ -79,21 +84,18 @@ jobs:
--name Cryptomator
--vendor "Skymatic GmbH"
--copyright "(C) 2016 - 2022 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
--java-options "--enable-preview"
--java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64"
--app-version "${{ steps.versions.outputs.semVerNum }}.${{ steps.versions.outputs.revNum }}"
--java-options "-Xss5m"
--java-options "-Xmx256m"
--java-options "-Dcryptomator.appVersion=\"${{ needs.get-version.outputs.semVerStr }}\""
--java-options "-Dcryptomator.appVersion=\"${{ steps.versions.outputs.semVerStr }}\""
--java-options "-Dfile.encoding=\"utf-8\""
--java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\""
--java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\""
--java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\""
--java-options "-Dcryptomator.p12Path=\"~/.config/Cryptomator/key.p12\""
--java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\""
--java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\""
--java-options "-Dcryptomator.showTrayIcon=false"
--java-options "-Dcryptomator.buildNumber=\"appimage-${{ needs.get-version.outputs.revNum }}\""
--java-options "-Dcryptomator.buildNumber=\"appimage-${{ steps.versions.outputs.revNum }}\""
--add-launcher Cryptomator-gtk2=launcher-gtk2.properties
--resource-dir dist/linux/resources
- name: Patch Cryptomator.AppDir
@@ -131,7 +133,7 @@ jobs:
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
- name: Build AppImage
run: >
./squashfs-root/AppRun Cryptomator.AppDir cryptomator-${{ needs.get-version.outputs.semVerStr }}-x86_64.AppImage
./squashfs-root/AppRun Cryptomator.AppDir cryptomator-${{ steps.versions.outputs.semVerStr }}-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: Create detached GPG signatures
+19 -6
View File
@@ -6,25 +6,26 @@ on:
types: [labeled]
env:
JAVA_VERSION: 19
JAVA_VERSION: 17
defaults:
run:
shell: bash
jobs:
jobs:
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@v3
- uses: actions/setup-java@v3
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
cache: 'maven'
- name: Cache SonarCloud packages
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
@@ -42,6 +43,15 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Sign source tarball with key 615D449FE6E6A235
if: startsWith(github.ref, 'refs/tags/')
run: |
git archive --prefix="cryptomator-${{ github.ref_name }}/" -o "cryptomator-${{ github.ref_name }}.tar.gz" ${{ github.ref }}
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.tar.gz
env:
GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
- name: Draft a release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
@@ -50,6 +60,9 @@ jobs:
discussion_category_name: releases
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
generate_release_notes: true
files: |
cryptomator-*.tar.gz.asc
fail_on_unmatched_files: true
body: |-
:construction: Work in Progress
+40 -74
View File
@@ -1,98 +1,72 @@
name: Build Debian Package
on:
release:
types: [published]
workflow_dispatch:
inputs:
ref:
description: 'GitHub Ref (e.g. refs/tags/1.6.16)'
required: true
semver:
description: 'SemVer String (e.g. 1.7.0-beta1)'
required: true
ppaver:
description: 'Base PPA Version String (e.g. 1.6.16+1.7.0~beta1) without -0ppa1'
required: true
dput:
description: 'Upload to PPA'
required: true
default: false
type: boolean
version:
description: 'Version'
required: false
env:
JAVA_VERSION: 19
OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/19/openjfx-19_linux-x64_bin-jmods.zip'
OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/19/openjfx-19_linux-aarch64_bin-jmods.zip'
JAVA_VERSION: 17
jobs:
build:
name: Build Debian Package
runs-on: ubuntu-20.04
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- id: versions
name: Get version information
run: |
SEM_VER_STR="${{ inputs.semver }}"
SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'`
REVCOUNT=`git rev-list --count HEAD`
echo "semVerStr=${SEM_VER_STR}" >> $GITHUB_OUTPUT
echo "semVerNum=${SEM_VER_NUM}" >> $GITHUB_OUTPUT
echo "revNum=${REVCOUNT}" >> $GITHUB_OUTPUT
- name: Install build tools
run: |
sudo add-apt-repository ppa:coffeelibs/openjdk
sudo apt-get update
sudo apt-get install debhelper devscripts dput coffeelibs-jdk-19 libgtk2.0-0
sudo apt-get install debhelper devscripts dput
- name: Setup Java
uses: actions/setup-java@v3
uses: actions/setup-java@v2
with:
distribution: 'zulu'
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
cache: 'maven'
- id: versions
name: Apply version information
run: |
if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR=${GITHUB_REF##*/}
mvn versions:set -DnewVersion=${SEM_VER_STR}
elif [[ "${{ github.event.inputs.version }}" =~ [0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR="${{ github.event.inputs.version }}"
mvn versions:set -DnewVersion=${SEM_VER_STR}
else
SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout`
fi
SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'`
REVCOUNT=`git rev-list --count HEAD`
echo "::set-output name=semVerStr::${SEM_VER_STR}"
echo "::set-output name=semVerNum::${SEM_VER_NUM}"
echo "::set-output name=revNum::${REVCOUNT}"
echo "::set-output name=ppaVerStr::${SEM_VER_STR/-/\~}-${REVCOUNT}"
- name: Validate Version
uses: skymatic/semver-validation-action@v1
with:
version: ${{ steps.versions.outputs.semVerStr }}
- name: Run maven
run: mvn -B clean package -Pdependency-check,linux -DskipTests
- name: Download OpenJFX jmods
id: download-jmods
run: |
curl -L ${{ env.OPENJFX_JMODS_AMD64 }} -o openjfx-amd64.zip
mkdir -p jmods/amd64
unzip -j openjfx-amd64.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d jmods/amd64
curl -L ${{ env.OPENJFX_JMODS_AARCH64 }} -o openjfx-aarch64.zip
mkdir -p jmods/aarch64
unzip -j openjfx-aarch64.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d jmods/aarch64
- name: Ensure major jfx version in pom and in jmods is the same
run: |
JMOD_VERSION_AMD64=$(jmod describe ${JAVA_HOME}/jmods/amd64/javafx.base.jmod | head -1)
JMOD_VERSION_AMD64=${JMOD_VERSION_AMD64#*@}
JMOD_VERSION_AMD64=${JMOD_VERSION_AMD64%%.*}
JMOD_VERSION_AARCH64=$(jmod describe ${JAVA_HOME}/jmods/aarch64/javafx.base.jmod | head -1)
JMOD_VERSION_AARCH64=${JMOD_VERSION_AARCH64#*@}
JMOD_VERSION_AARCH64=${JMOD_VERSION_AARCH64%%.*}
POM_JFX_VERSION=$(mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout)
POM_JFX_VERSION=${POM_JFX_VERSION#*@}
POM_JFX_VERSION=${POM_JFX_VERSION%%.*}
if [ $POM_JFX_VERSION -ne $JMOD_VERSION_AMD64 ]; then
>&2 echo "Major JavaFX version in pom.xml (${POM_JFX_VERSION}) != amd64 jmod version (${JMOD_VERSION_AMD64})"
exit 1
fi
if [ $POM_JFX_VERSION -ne $JMOD_VERSION_AARCH64 ]; then
>&2 echo "Major JavaFX version in pom.xml (${POM_JFX_VERSION}) != aarch64 jmod version (${JMOD_VERSION_AARCH64})"
exit 1
fi
- name: Create orig.tar.gz with common/ libs/ mods/ jmods/
- name: Create orig.tar.gz with common/ libs/ mods/
run: |
mkdir pkgdir
cp -r target/libs pkgdir
cp -r target/mods pkgdir
cp -r jmods pkgdir
cp -r dist/linux/common/ pkgdir
cp target/cryptomator-*.jar pkgdir/mods
tar -cJf cryptomator_${{ inputs.ppaver }}.orig.tar.xz -C pkgdir .
tar -cJf cryptomator_${{ steps.versions.outputs.ppaVerStr }}.orig.tar.xz -C pkgdir .
- name: Patch and rename pkgdir
run: |
cp -r dist/linux/debian/ pkgdir
@@ -100,12 +74,12 @@ jobs:
envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM}' < dist/linux/debian/rules > pkgdir/debian/rules
envsubst '${PPA_VERSION} ${RFC2822_TIMESTAMP}' < dist/linux/debian/changelog > pkgdir/debian/changelog
find . -name "*.jar" >> pkgdir/debian/source/include-binaries
mv pkgdir cryptomator_${{ inputs.ppaver }}
mv pkgdir cryptomator_${{ steps.versions.outputs.ppaVerStr }}
env:
SEMVER_STR: ${{ steps.versions.outputs.semVerStr }}
VERSION_NUM: ${{ steps.versions.outputs.semVerNum }}
REVISION_NUM: ${{ steps.versions.outputs.revNum }}
PPA_VERSION: ${{ inputs.ppaver }}-0ppa1
PPA_VERSION: ${{ steps.versions.outputs.ppaVerStr }}-0ppa1
- name: Prepare GPG-Agent for signing with key 615D449FE6E6A235
run: |
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
@@ -120,7 +94,7 @@ jobs:
env:
DEBSIGN_PROGRAM: gpg --batch --pinentry-mode loopback
DEBSIGN_KEYID: 615D449FE6E6A235
working-directory: cryptomator_${{ inputs.ppaver }}
working-directory: cryptomator_${{ steps.versions.outputs.ppaVerStr }}
- name: Create detached GPG signatures
run: |
gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator_*_amd64.deb
@@ -137,21 +111,13 @@ jobs:
cryptomator_*_amd64.deb
cryptomator_*.asc
- name: Publish on PPA
if: inputs.dput
if: startsWith(github.ref, 'refs/tags/') || github.event.inputs.dput == 'true'
run: dput ppa:sebastian-stenzel/cryptomator-beta cryptomator_*_source.changes
# If ref is a tag, also upload to GitHub Releases:
- name: Determine tag name
if: startsWith(inputs.ref, 'refs/tags/')
run: |
REF=${{ inputs.ref }}
echo "TAG_NAME=${REF##*/}" >> $GITHUB_ENV
- name: Publish Debian package on GitHub Releases
if: startsWith(inputs.ref, 'refs/tags/')
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
with:
fail_on_unmatched_files: true
tag_name: ${{ github.env.TAG_NAME }}
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
files: |
cryptomator_*_amd64.deb
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
jq -c 'select(.filename|endswith(".dmg")) | select(.filename|endswith("-arm64.dmg")|not) | {name: "github.releases.downloads", tags: ["file=dmg", "version=\(.release)", "arch=amd64"], value: .downloads, interval: .interval, time: .time}' input.json >> output.json
RESULT=$(jq -s -c "." output.json)
echo "result=${RESULT}" >> $GITHUB_OUTPUT
echo "::set-output name=result::${RESULT}"
env:
INTERVAL: 900
JSON_DATA: ${{ steps.get-stats.outputs.result }}
-60
View File
@@ -1,60 +0,0 @@
name: Update Error Database
on:
discussion:
types: [created, edited, category_changed, answered, unanswered]
discussion_comment:
types: [created, edited, deleted]
jobs:
update-error-db:
runs-on: ubuntu-latest
if: github.event.discussion.category.name == 'Errors'
steps:
- name: Query Discussion Data
id: query-data
uses: actions/github-script@v6
with:
script: |
const query = `query ($owner: String!, $name: String!, $discussionNumber: Int!) {
repository(owner: $owner, name: $name) {
discussion(number: $discussionNumber) {
id
upvoteCount
title
url
answer {
url
upvoteCount
}
comments {
totalCount
}
}
}
}`;
const variables = {
owner: context.repo.owner,
name: context.repo.repo,
discussionNumber: context.payload.discussion.number
}
return await github.graphql(query, variables)
- name: Get Gist
id: get-gist
uses: andymckay/get-gist-action@master
with:
gistURL: https://gist.github.com/cryptobot/accba9fb9555e7192271b85606f97230
- name: Merge Error Code Data
run: |
jq -c '.' ${{ steps.get-gist.outputs.file }} > original.json
echo $DISCUSSION | jq -c '.repository.discussion | .comments = .comments.totalCount | {(.id|tostring) : .}' > new.json
jq -s '.[0] * .[1]' original.json new.json > merged.json
env:
DISCUSSION: ${{ steps.query-data.outputs.result }}
- name: Patch Gist
uses: exuanbo/actions-deploy-gist@v1
with:
token: ${{ secrets.CRYPTOBOT_GIST_TOKEN }}
gist_id: accba9fb9555e7192271b85606f97230
gist_file_name: errorcodes.json
file_path: merged.json
-77
View File
@@ -1,77 +0,0 @@
name: Parse and Validate a version string or tag
on:
workflow_call:
inputs:
version:
description: "A specific version to use"
required: false
type: string
outputs:
semVerStr:
description: "The full version string."
value: ${{ jobs.determine-version.outputs.semVerStr}}
semVerNum:
description: "The numerical part of the version string"
value: ${{ jobs.determine-version.outputs.semVerNum}}
revNum:
description: "The revision number"
value: ${{ jobs.determine-version.outputs.revNum}}
versionType:
description: "Type of the version. Values are [stable, alpha, beta, rc, unknown]"
value: ${{ jobs.determine-version.outputs.type }}
env:
JAVA_VERSION: 19
JAVA_DIST: 'temurin'
JAVA_CACHE: 'maven'
jobs:
determine-version:
name: 'Determines the version following semver'
runs-on: ubuntu-latest
outputs:
semVerNum: ${{ steps.versions.outputs.semVerNum }}
semVerStr: ${{ steps.versions.outputs.semVerStr }}
revNum: ${{ steps.versions.outputs.revNum }}
type: ${{ steps.versions.outputs.type}}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
cache: ${{ env.JAVA_CACHE }}
- id: versions
name: Get version information
run: |
if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR=${GITHUB_REF##*/}
elif [[ "${{ inputs.version }}" =~ [0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR="${{ github.event.inputs.version }}"
else
SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout`
fi
SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'`
REVCOUNT=`git rev-list --count HEAD`
TYPE="unknown"
if [[ $SEM_VER_STR =~ [0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TYPE="stable"
elif [[ $SEM_VER_STR =~ [0-9]+\.[0-9]+\.[0-9]+-alpha[1-9]+$ ]]; then
TYPE="alpha"
elif [[ $SEM_VER_STR =~ [0-9]+\.[0-9]+\.[0-9]+-beta[1-9]+$ ]]; then
TYPE="beta"
elif [[ $SEM_VER_STR =~ [0-9]+\.[0-9]+\.[0-9]+-rc[1-9]$ ]]; then
TYPE="rc"
fi
echo "semVerStr=${SEM_VER_STR}" >> $GITHUB_OUTPUT
echo "semVerNum=${SEM_VER_NUM}" >> $GITHUB_OUTPUT
echo "revNum=${REVCOUNT}" >> $GITHUB_OUTPUT
echo "type=${TYPE}" >> $GITHUB_OUTPUT
- name: Validate Version
uses: skymatic/semver-validation-action@v2
with:
version: ${{ steps.versions.outputs.semVerStr }}
+41 -56
View File
@@ -10,52 +10,43 @@ on:
required: false
env:
JAVA_VERSION: 19
JAVA_VERSION: 17
jobs:
get-version:
uses: ./.github/workflows/get-version.yml
with:
version: ${{ github.event.inputs.version }}
build:
name: Build Cryptomator.app for ${{ matrix.output-suffix }}
runs-on: ${{ matrix.os }}
needs: [get-version]
strategy:
fail-fast: false
matrix:
include:
- os: macos-11
architecture: x64
output-suffix: x64
xcode-path: '/Applications/Xcode_13.2.1.app'
- os: [self-hosted, macOS, ARM64]
architecture: aarch64
output-suffix: arm64
xcode-path: '/Applications/Xcode_13.2.1.app'
name: Build Cryptomator.app
runs-on: macos-11
steps:
- uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
- uses: actions/checkout@v2
with:
distribution: 'zulu'
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
java-package: 'jdk+fx'
architecture: ${{ matrix.architecture }}
cache: 'maven'
- name: Ensure major jfx version in pom equals in jdk
if: ${{ !contains(matrix.os, 'self-hosted') }}
shell: pwsh
- id: versions
name: Apply version information
run: |
$jfxPomVersion = (&mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout) -split "\."
$jfxJdkVersion = ((Get-Content -path "${env:JAVA_HOME}/lib/javafx.properties" | Where-Object {$_ -like 'javafx.version=*' }) -replace '.*=','') -split "\."
if ($jfxPomVersion[0] -ne $jfxJdkVersion[0]) {
Write-Error "Major part of JavaFX version in pom($($jfxPomVersion[0])) does not match the version in JDK($($jfxJdkVersion[0])) "
exit 1
}
- name: Set version
run : mvn versions:set -DnewVersion=${{ needs.get-version.outputs.semVerStr }}
if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR=${GITHUB_REF##*/}
mvn versions:set -DnewVersion=${SEM_VER_STR}
elif [[ "${{ github.event.inputs.version }}" =~ [0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR="${{ github.event.inputs.version }}"
mvn versions:set -DnewVersion=${SEM_VER_STR}
else
SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout`
fi
SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'`
REVCOUNT=`git rev-list --count HEAD`
echo "::set-output name=semVerStr::${SEM_VER_STR}"
echo "::set-output name=semVerNum::${SEM_VER_NUM}"
echo "::set-output name=revNum::${REVCOUNT}"
- name: Validate Version
uses: skymatic/semver-validation-action@v1
with:
version: ${{ steps.versions.outputs.semVerStr }}
- name: Run maven
run: mvn -B clean package -Pdependency-check,mac -DskipTests
- name: Patch target dir
@@ -69,7 +60,7 @@ jobs:
--verbose
--output runtime
--module-path "${JAVA_HOME}/jmods"
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--strip-native-commands
--no-header-files
--no-man-pages
@@ -88,23 +79,20 @@ jobs:
--name Cryptomator
--vendor "Skymatic GmbH"
--copyright "(C) 2016 - 2022 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}"
--java-options "--enable-preview"
--java-options "--enable-native-access=org.cryptomator.jfuse.mac"
--app-version "${{ steps.versions.outputs.semVerNum }}"
--java-options "-Xss5m"
--java-options "-Xmx256m"
--java-options "-Dfile.encoding=\"utf-8\""
--java-options "-Dapple.awt.enableTemplateImages=true"
--java-options "-Dsun.java2d.metal=true"
--java-options "-Dcryptomator.appVersion=\"${{ needs.get-version.outputs.semVerStr }}\""
--java-options "-Dcryptomator.appVersion=\"${{ steps.versions.outputs.semVerStr }}\""
--java-options "-Dcryptomator.logDir=\"~/Library/Logs/Cryptomator\""
--java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/Cryptomator/Plugins\""
--java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\""
--java-options "-Dcryptomator.p12Path=\"~/Library/Application Support/Cryptomator/key.p12\""
--java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/Cryptomator/ipc.socket\""
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
--java-options "-Dcryptomator.showTrayIcon=true"
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\""
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ steps.versions.outputs.revNum }}\""
--mac-package-identifier org.cryptomator
--resource-dir dist/mac/resources
- name: Patch Cryptomator.app
@@ -114,8 +102,8 @@ jobs:
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.get-version.outputs.semVerNum }}
REVISION_NO: ${{ needs.get-version.outputs.revNum }}
VERSION_NO: ${{ steps.versions.outputs.semVerNum }}
REVISION_NO: ${{ steps.versions.outputs.revNum }}
- name: Generate license for dmg
run: >
mvn -B license:add-third-party
@@ -149,10 +137,6 @@ jobs:
CODESIGN_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_CODESIGN_TMP_KEYCHAIN_PW }}
- name: Codesign
run: |
echo "Codesigning jdk files..."
find Cryptomator.app/Contents/runtime/Contents/Home/lib/ -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \;
find Cryptomator.app/Contents/runtime/Contents/Home/lib/ -name 'jspawnhelper' -exec codesign --force -o runtime -s ${CODESIGN_IDENTITY} {} \;
echo "Codesigning jar contents..."
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
@@ -201,9 +185,9 @@ jobs:
--icon ".background" 128 758
--icon ".fseventsd" 320 758
--icon ".VolumeIcon.icns" 512 758
Cryptomator-${VERSION_NO}-${{ matrix.output-suffix }}.dmg dmg
Cryptomator-${VERSION_NO}.dmg dmg
env:
VERSION_NO: ${{ needs.get-version.outputs.semVerNum }}
VERSION_NO: ${{ steps.versions.outputs.semVerNum }}
- name: Notarize .dmg
if: startsWith(github.ref, 'refs/tags/')
uses: cocoalibs/xcode-notarization-action@v1
@@ -212,9 +196,8 @@ jobs:
apple-id: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
password: ${{ secrets.MACOS_NOTARIZATION_PW }}
team-id: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
xcode-path: ${{ matrix.xcode-path }}
- name: Add possible alpha/beta tags to installer name
run: mv Cryptomator-*.dmg Cryptomator-${{ needs.get-version.outputs.semVerStr }}-${{ matrix.output-suffix }}.dmg
run: mv Cryptomator-*.dmg Cryptomator-${{ steps.versions.outputs.semVerStr }}.dmg
- name: Create detached GPG signature with key 615D449FE6E6A235
run: |
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
@@ -229,7 +212,7 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: dmg-${{ matrix.output-suffix }}
name: dmg
path: Cryptomator-*.dmg
if-no-files-found: error
- name: Publish dmg on GitHub Releases
@@ -241,3 +224,5 @@ jobs:
files: |
Cryptomator-*.dmg
Cryptomator-*.asc
-39
View File
@@ -1,39 +0,0 @@
name: Post Release Publish Tasks
on:
release:
types: [published]
jobs:
get-version:
runs-on: ubuntu-latest
steps:
- name: Download source tarball
run: |
curl -L -H "Accept: application/vnd.github+json" ${{ github.event.release.tarball_url }} --output cryptomator-${{ github.event.release.tag_name }}.tar.gz
- name: Sign source tarball 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 --detach-sign -a cryptomator-*.tar.gz
env:
GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
- name: Publish asc on GitHub Releases
uses: softprops/action-gh-release@v1
with:
fail_on_unmatched_files: true
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
files: |
cryptomator-*.tar.gz.asc
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_USERNAME: 'Cryptobot'
SLACK_ICON: false
SLACK_ICON_EMOJI: ':bot:'
SLACK_CHANNEL: 'cryptomator-desktop'
SLACK_TITLE: "Release ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} published."
SLACK_MESSAGE: "Ready to <https://github.com/${{ github.repository }}/actions/workflows/debian.yml|build deb Package>."
SLACK_FOOTER: false
MSG_MINIMAL: true
+4 -4
View File
@@ -4,7 +4,7 @@ on:
pull_request:
env:
JAVA_VERSION: 19
JAVA_VERSION: 17
defaults:
run:
@@ -16,10 +16,10 @@ jobs:
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@v3
- uses: actions/setup-java@v3
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
cache: 'maven'
- name: Build and Test
-43
View File
@@ -1,43 +0,0 @@
name: Release Check
on:
push:
branches:
- 'release/**'
- 'hotfix/**'
env:
JAVA_VERSION: 19
defaults:
run:
shell: bash
jobs:
release-check-precondition:
name: Validate commits pushed to release/hotfix branch to fulfill release requirements
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- id: validate-pom-version
name: Validate POM version
run: |
if [[ $GITHUB_REF =~ refs/heads/(hotfix|release)/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR=${GITHUB_REF##*/}
else
echo "Failed to parse version"
exit 1
fi
if [[ ${SEM_VER_STR} == `mvn help:evaluate -Dexpression=project.version -q -DforceStdout` ]]; then
echo "semVerStr=${SEM_VER_STR}" >> $GITHUB_OUTPUT
else
echo "Version not set in POM"
exit 1
fi
- name: Validate release in org.cryptomator.Cryptomator.metainfo.xml file
run: |
if ! grep -q "<release date=\".*\" version=\"${{ steps.validate-pom-version.outputs.semVerStr }}\"/>" dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml; then
echo "Release not set in dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml"
exit 1
fi
+73 -79
View File
@@ -10,46 +10,48 @@ on:
required: false
env:
JAVA_VERSION: 19
JAVA_DIST: 'zulu'
JAVA_CACHE: 'maven'
JAVA_VERSION: 17
WINFSP_MSI: https://github.com/winfsp/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi
defaults:
run:
shell: bash
jobs:
get-version:
uses: ./.github/workflows/get-version.yml
with:
version: ${{ github.event.inputs.version }}
build-msi:
name: Build .msi Installer
runs-on: windows-latest
needs: [get-version]
env:
LOOPBACK_ALIAS: 'cryptomator-vault'
steps:
- uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
- uses: actions/checkout@v2
with:
distribution: ${{ env.JAVA_DIST }}
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
java-package: 'jdk+fx'
cache: ${{ env.JAVA_CACHE }}
- name: Ensure major jfx version in pom equals in jdk
shell: pwsh
cache: 'maven'
- id: versions
name: Apply version information
run: |
$jfxPomVersion = (&mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout) -split "\."
$jfxJdkVersion = ((Get-Content -path "${env:JAVA_HOME}/lib/javafx.properties" | Where-Object {$_ -like 'javafx.version=*' }) -replace '.*=','') -split "\."
if ($jfxPomVersion[0] -ne $jfxJdkVersion[0]) {
Write-Error "Major part of JavaFX version in pom($($jfxPomVersion[0])) does not match the version in JDK($($jfxJdkVersion[0])) "
exit 1
}
- name: Set version
run : mvn versions:set -DnewVersion=${{ needs.get-version.outputs.semVerStr }}
if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR=${GITHUB_REF##*/}
mvn versions:set -DnewVersion=${SEM_VER_STR}
elif [[ "${{ github.event.inputs.version }}" =~ [0-9]+\.[0-9]+\.[0-9]+.* ]]; then
SEM_VER_STR="${{ github.event.inputs.version }}"
mvn versions:set -DnewVersion=${SEM_VER_STR}
else
SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout`
fi
SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'`
REVCOUNT=`git rev-list --count HEAD`
echo "::set-output name=semVerStr::${SEM_VER_STR}"
echo "::set-output name=semVerNum::${SEM_VER_NUM}"
echo "::set-output name=revNum::${REVCOUNT}"
- name: Validate Version
uses: skymatic/semver-validation-action@v1
with:
version: ${{ steps.versions.outputs.semVerStr }}
- name: Run maven
run: mvn -B clean package -Pdependency-check,win -DskipTests
- name: Patch target dir
@@ -63,7 +65,7 @@ jobs:
--verbose
--output runtime
--module-path "${JAVA_HOME}/jmods"
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--strip-native-commands
--no-header-files
--no-man-pages
@@ -82,48 +84,34 @@ jobs:
--name Cryptomator
--vendor "Skymatic GmbH"
--copyright "(C) 2016 - 2022 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
--java-options "--enable-preview"
--java-options "--enable-native-access=org.cryptomator.jfuse.win"
--app-version "${{ steps.versions.outputs.semVerNum }}.${{ steps.versions.outputs.revNum }}"
--java-options "-Xss5m"
--java-options "-Xmx256m"
--java-options "-Dcryptomator.appVersion=\"${{ needs.get-version.outputs.semVerStr }}\""
--java-options "-Dcryptomator.appVersion=\"${{ steps.versions.outputs.semVerStr }}\""
--java-options "-Dfile.encoding=\"utf-8\""
--java-options "-Dcryptomator.logDir=\"~/AppData/Roaming/Cryptomator\""
--java-options "-Dcryptomator.pluginDir=\"~/AppData/Roaming/Cryptomator/Plugins\""
--java-options "-Dcryptomator.settingsPath=\"~/AppData/Roaming/Cryptomator/settings.json\""
--java-options "-Dcryptomator.p12Path=\"~/AppData/Roaming/Cryptomator/key.p12\""
--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.loopbackAlias=\"${{ env.LOOPBACK_ALIAS }}\""
--java-options "-Dcryptomator.showTrayIcon=true"
--java-options "-Dcryptomator.buildNumber=\"msi-${{ needs.get-version.outputs.revNum }}\""
--java-options "-Dcryptomator.buildNumber=\"msi-${{ steps.versions.outputs.revNum }}\""
--java-options "-Dcryptomator.integrationsWin.autoStartShellLinkName=\"Cryptomator\""
--java-options "-Dcryptomator.integrationsWin.keychainPaths=\"~/AppData/Roaming/Cryptomator/keychain.json\""
--resource-dir dist/win/resources
--icon dist/win/resources/Cryptomator.ico
- name: Patch Application Directory
run: |
cp dist/win/contrib/* appdir/Cryptomator
- name: Set LOOPBACK_ALIAS in patchWebDAV.bat
shell: pwsh
run: |
$patchScript = "appdir\Cryptomator\patchWebDAV.bat"
try {
(Get-Content $patchScript ) -replace '::REPLACE ME', "SET LOOPBACK_ALIAS=`"${{ env.LOOPBACK_ALIAS}}`"" | Set-Content $patchScript
} catch {
Write-Host "Failed to set LOOPBACK_ALIAS for patchWebDAV.bat"
exit 1
}
- name: Fix permissions
run: attrib -r appdir/Cryptomator/Cryptomator.exe
shell: pwsh
- name: Codesign
uses: skymatic/code-sign-action@v2
uses: skymatic/code-sign-action@v1
with:
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
certificatesha1: 5FC94CE149E5B511E621F53A060AC67CBD446B3A
certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B
description: Cryptomator
timestampUrl: 'http://timestamp.digicert.com'
folder: appdir/Cryptomator
@@ -150,7 +138,7 @@ jobs:
--name Cryptomator
--vendor "Skymatic GmbH"
--copyright "(C) 2016 - 2022 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}"
--app-version "${{ steps.versions.outputs.semVerNum }}"
--win-menu
--win-dir-chooser
--win-shortcut-prompt
@@ -162,16 +150,16 @@ jobs:
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@v2
uses: skymatic/code-sign-action@v1
with:
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
certificatesha1: 5FC94CE149E5B511E621F53A060AC67CBD446B3A
certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B
description: Cryptomator Installer
timestampUrl: 'http://timestamp.digicert.com'
folder: installer
- name: Add possible alpha/beta tags to installer name
run: mv installer/Cryptomator-*.msi Cryptomator-${{ needs.get-version.outputs.semVerStr }}-x64.msi
run: mv installer/Cryptomator-*.msi Cryptomator-${{ steps.versions.outputs.semVerStr }}-x64.msi
- name: Create detached GPG signature with key 615D449FE6E6A235
run: |
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
@@ -196,34 +184,42 @@ jobs:
files: |
*.msi
*.asc
outputs:
semVerNum: ${{ steps.versions.outputs.semVerNum }}
semVerStr: ${{ steps.versions.outputs.semVerStr }}
revNum: ${{ steps.versions.outputs.revNum }}
call-winget-flow:
needs: [get-version, build-msi]
if: github.event.action == 'published' && needs.get-version.outputs.versionType == 'stable'
uses: ./.github/workflows/winget.yml
with:
releaseTag: ${{ github.event.release.tag_name }}
secrets: inherit
publish-winget:
name: Publish on winget repo
runs-on: windows-latest
needs: [build-msi]
steps:
- name: Submit package to Windows Package Manager Community Repository
run: |
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
$github = Get-Content '${{ github.event_path }}' | ConvertFrom-Json
$installerUrl = $github.release.assets | Where-Object -Property name -match 'Cryptomator-*.msi' | Select -ExpandProperty browser_download_url -First 1
.\wingetcreate.exe update Cryptomator.Cryptomator -s -v $github.release.tag_name -u $installerUrl -t ${{ secrets.CRYPTOBOT_WINGET_TOKEN }}
shell: pwsh
build-exe:
name: Build .exe installer
runs-on: windows-latest
needs: [get-version, build-msi]
needs: [build-msi]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Download .msi
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: msi
path: dist/win/bundle/resources
- name: Strip version info from msi file name
run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi
- uses: actions/setup-java@v3
- uses: actions/setup-java@v2
with:
distribution: ${{ env.JAVA_DIST }}
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
cache: ${{ env.JAVA_CACHE }}
cache: 'maven'
- name: Generate license for exe
run: >
mvn -B license:add-third-party
@@ -236,16 +232,14 @@ jobs:
"-Dlicense.licenseMergesUrl=file:///${{ github.workspace }}/license/merges"
shell: pwsh
- name: Download WinFsp
run: |
$winfspUrl= (Select-String -Path ".\dist\win\bundle\resources\winfsp-download.url" -Pattern 'https:.*').Matches.Value
curl --output dist/win/bundle/resources/winfsp.msi -L $winfspUrl
shell: pwsh
run:
curl --output dist/win/bundle/resources/winfsp.msi -L ${{ env.WINFSP_MSI }}
- name: Compile to wixObj file
run: >
"${WIX}/bin/candle.exe" dist/win/bundle/bundleWithWinfsp.wxs
-ext WixBalExtension
-out dist/win/bundle/
-dBundleVersion="${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
-dBundleVersion="${{ needs.build-msi.outputs.semVerNum }}.${{ needs.build-msi.outputs.revNum }}"
-dBundleVendor="Skymatic GmbH"
-dBundleCopyright="(C) 2016 - 2022 Skymatic GmbH"
-dAboutUrl="https://cryptomator.org"
@@ -262,11 +256,11 @@ jobs:
-ib installer/unsigned/Cryptomator-Installer.exe
-o tmp/engine.exe
- name: Codesign burn engine
uses: skymatic/code-sign-action@v2
uses: skymatic/code-sign-action@v1
with:
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
certificatesha1: 5FC94CE149E5B511E621F53A060AC67CBD446B3A
certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B
description: Cryptomator Installer
timestampUrl: 'http://timestamp.digicert.com'
folder: tmp
@@ -276,16 +270,16 @@ jobs:
-ab tmp/engine.exe installer/unsigned/Cryptomator-Installer.exe
-o installer/Cryptomator-Installer.exe
- name: Codesign EXE
uses: skymatic/code-sign-action@v2
uses: skymatic/code-sign-action@v1
with:
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
certificatesha1: 5FC94CE149E5B511E621F53A060AC67CBD446B3A
certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B
description: Cryptomator Installer
timestampUrl: 'http://timestamp.digicert.com'
folder: installer
- name: Add possible alpha/beta tags to installer name
run: mv installer/Cryptomator-Installer.exe Cryptomator-${{ needs.get-version.outputs.semVerStr }}-x64.exe
run: mv installer/Cryptomator-Installer.exe Cryptomator-${{ needs.build-msi.outputs.semVerStr }}-x64.exe
- name: Create detached GPG signature with key 615D449FE6E6A235
run: |
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
@@ -318,12 +312,12 @@ jobs:
needs: [build-msi, build-exe]
steps:
- name: Download .msi
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: msi
path: msi
- name: Download .exe
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: exe
path: exe
@@ -333,7 +327,7 @@ jobs:
cp msi/*.msi files
cp exe/*.exe files
- name: Upload to Kaspersky
uses: SamKirkland/FTP-Deploy-Action@4.3.3
uses: SamKirkland/FTP-Deploy-Action@4.3.0
with:
protocol: ftps
server: allowlist.kaspersky-labs.com
-49
View File
@@ -1,49 +0,0 @@
name: Release to Winget
on:
workflow_call:
inputs:
releaseTag:
required: true
type: string
workflow_dispatch:
inputs:
releaseTag:
description: 'Release tag name'
required: true
type: string
jobs:
publish-winget:
name: Publish on winget repo
runs-on: windows-latest
steps:
- name: Get download url for release assets
id: get-release-assets
uses: actions/github-script@v6
with:
script: |
const query =`query($tag:String!) {
repository(owner:"cryptomator", name:"cryptomator"){
release(tagName: $tag) {
releaseAssets(first:20) {
nodes {
name
downloadUrl
}
}
}
}
}`;
const variables = {
tag: "${{ inputs.releaseTag }}"
}
return await github.graphql(query, variables)
- name: Submit package to Windows Package Manager Community Repository
id: submit-winget
run: |
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
$releaseAssets = (ConvertFrom-Json '${{ steps.get-release-assets.outputs.result }}').repository.release.releaseAssets.nodes
$installerUrl = $releaseAssets | Where-Object -Property name -match '^Cryptomator-.*\.msi$' | Select -ExpandProperty downloadUrl -First 1
.\wingetcreate.exe update Cryptomator.Cryptomator -s -v "${{ inputs.releaseTag }}" -u "$installerUrl" -t ${{ secrets.CRYPTOBOT_WINGET_TOKEN }}
shell: pwsh
+11 -11
View File
@@ -14,10 +14,10 @@
<option name="dagger.fastInit" value="enabled" />
<option name="dagger.formatGeneratedSource" value="enabled" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.44/dagger-compiler-2.44.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.44/dagger-2.44.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.41/dagger-compiler-2.41.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.41/dagger-2.41.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.44/dagger-producers-2.44.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.41/dagger-producers-2.41.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/31.0.1-jre/guava-31.0.1-jre.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
@@ -26,18 +26,18 @@
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.7.1/error_prone_annotations-2.7.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.5/checker-compat-qual-2.5.5.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.44/dagger-spi-2.44.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/devtools/ksp/symbol-processing-api/1.7.0-1.0.6/symbol-processing-api-1.7.0-1.0.6.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.7.0/kotlin-stdlib-1.7.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.7.0/kotlin-stdlib-common-1.7.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.41/dagger-spi-2.41.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/devtools/ksp/symbol-processing-api/1.5.30-1.0.0/symbol-processing-api-1.5.30-1.0.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.5.32/kotlin-stdlib-jdk8-1.5.32.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.5.32/kotlin-stdlib-1.5.32.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.5.32/kotlin-stdlib-common-1.5.32.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.5.32/kotlin-stdlib-jdk7-1.5.32.jar" />
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.13.0/javapoet-1.13.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.7.0/kotlin-stdlib-jdk8-1.7.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.7.0/kotlin-stdlib-jdk7-1.7.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
<entry name="$MAVEN_REPOSITORY$/net/ltgt/gradle/incap/incap/0.2/incap-0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.5.0/kotlinx-metadata-jvm-0.5.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.3.0/kotlinx-metadata-jvm-0.3.0.jar" />
</processorPath>
<module name="cryptomator" />
</profile>
@@ -45,7 +45,7 @@
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="cryptomator" options="-Adagger.fastInit=enabled -Adagger.formatGeneratedSource=enabled --enable-preview" />
<module name="cryptomator" options="-Adagger.fastInit=enabled -Adagger.formatGeneratedSource=enabled" />
</option>
</component>
</project>
+1 -1
View File
@@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" default="true" project-jdk-name="19" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.p12Path=&quot;~/.config/Cryptomator/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.p12Path=&quot;~/.config/Cryptomator-Dev/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator-Dev/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dcryptomator.showTrayIcon=true -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator-Dev/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dcryptomator.showTrayIcon=true -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator/Plugins&quot; -Dcryptomator.integrationsWin.keychainPaths=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Dcryptomator.p12Path=&quot;~/AppData/Roaming/Cryptomator/key.p12&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.win" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator/Plugins&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator-Dev/Plugins&quot; -Dcryptomator.integrationsWin.keychainPaths=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.p12Path=&quot;~/AppData/Roaming/Cryptomator-Dev/key.p12&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.win" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator-Dev/Plugins&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>
+1 -1
View File
@@ -5,7 +5,7 @@
</envs>
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.p12Path=&quot;~/Library/Application Support/Cryptomator/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator/Plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac" />
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator/Plugins&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
<method v="2">
<option name="Make" enabled="true" />
</method>
+1 -1
View File
@@ -5,7 +5,7 @@
</envs>
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.p12Path=&quot;~/Library/Application Support/Cryptomator-Dev/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator-Dev/Plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac" />
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator-Dev/Plugins&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
<method v="2">
<option name="Make" enabled="true" />
</method>
+7 -10
View File
@@ -32,17 +32,10 @@ Cryptomator is provided free of charge as an open-source project despite the hig
<tbody>
<tr>
<td><a href="https://mowcapital.com/"><img src="https://cryptomator.org/img/sponsors/mowcapital.svg" alt="Mow Capital" height="40"></a></td>
<td><a href="https://www.easeus.com/"><img src="https://cryptomator.org/img/sponsors/easeus.png" alt="EaseUS" height="40"></a></td>
</tr>
</tbody>
</table>
### Special Shoutout
Continuous integration hosting for ARM64 builds is provided by [MacStadium](https://www.macstadium.com/opensource).
<a href="https://www.macstadium.com/opensource"><img src="https://uploads-ssl.webflow.com/5ac3c046c82724970fc60918/5c019d917bba312af7553b49_MacStadium-developerlogo.png" alt="MacStadium" height="100"></a>
---
## Introduction
@@ -61,7 +54,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
- File names get encrypted
- Folder structure gets obfuscated
- Use as many vaults in your Dropbox as you want, each having individual passwords
- Four thousand commits for the security of your data!! :tada:
- Three thousand commits for the security of your data!! :tada:
### Privacy
@@ -85,20 +78,24 @@ For more information on the security details visit [cryptomator.org](https://doc
### Dependencies
* JDK 19 (e.g. temurin)
* JDK 17 (e.g. temurin)
* Maven 3
### Run Maven
```
mvn clean install
# or mvn clean install -Pwin
# or mvn clean install -Pwindows
# or mvn clean install -Pmac
# or mvn clean install -Plinux
```
This will build all the jars and bundle them together with their OS-specific dependencies under `target`. This can now be used to build native packages.
### Start Cryptomator
If you unzip the buildkit for your OS, you will find a launcher script with some basic settings. You might want to adjust these to your needs. To start Cryptomator, simply execute the launcher script from a terminal, e.g. `launcher-linux.sh`, if you're on a Linux system.
## License
This project is dual-licensed under the GPLv3 for FOSS projects as well as a commercial license for independent software vendors and resellers. If you want to modify this application under different conditions, feel free to contact our support team.
+1 -3
View File
@@ -19,7 +19,7 @@ cp ../../../target/cryptomator-*.jar ../../../target/mods
${JAVA_HOME}/bin/jlink \
--output runtime \
--module-path "${JAVA_HOME}/jmods" \
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--strip-native-commands \
--no-header-files \
--no-man-pages \
@@ -39,8 +39,6 @@ ${JAVA_HOME}/bin/jpackage \
--name Cryptomator \
--vendor "Skymatic GmbH" \
--copyright "(C) 2016 - 2022 Skymatic GmbH" \
--java-options "--enable-preview" \
--java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" \
--java-options "-Xss5m" \
--java-options "-Xmx256m" \
--app-version "${VERSION}.${REVISION_NO}" \
+2 -2
View File
@@ -19,7 +19,7 @@ fi
export LD_PRELOAD=lib/app/libjffi.so
if [ "$GTK2_PRESENT" -eq 0 ] && [ "$GTK3_PRESENT" -ne 0 ]; then
bin/Cryptomator-gtk2 $@
bin/Cryptomator-gtk2
else
bin/Cryptomator $@
bin/Cryptomator
fi
@@ -66,11 +66,6 @@
</content_rating>
<releases>
<release date="2022-12-06" version="1.6.16"/>
<release date="2022-10-06" version="1.6.15"/>
<release date="2022-08-31" version="1.6.14"/>
<release date="2022-07-27" version="1.6.12"/>
<release date="2022-07-26" version="1.6.11"/>
<release date="2022-05-03" version="1.6.10"/>
<release date="2022-04-27" version="1.6.9"/>
<release date="2022-03-30" version="1.6.8"/>
+1 -1
View File
@@ -1,4 +1,4 @@
cryptomator (${PPA_VERSION}) focal; urgency=low
cryptomator (${PPA_VERSION}) bionic; urgency=low
* Full changelog can be found on https://github.com/cryptomator/cryptomator/releases
+1 -1
View File
@@ -2,7 +2,7 @@ Source: cryptomator
Maintainer: Cryptobot <releases@cryptomator.org>
Section: utils
Priority: optional
Build-Depends: debhelper (>=10), coffeelibs-jdk-19, libgtk2.0-0, libgtk-3-0, libxxf86vm1, libgl1
Build-Depends: debhelper (>=10), openjdk-17-jdk
Standards-Version: 4.5.0
Homepage: https://cryptomator.org
Vcs-Git: https://github.com/cryptomator/cryptomator.git
+1 -1
View File
@@ -3,4 +3,4 @@
# fix for https://github.com/cryptomator/cryptomator/issues/1370
export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/jni/libjffi-1.2.so
/usr/lib/cryptomator/bin/cryptomator $@
/usr/lib/cryptomator/bin/cryptomator
+3 -15
View File
@@ -4,14 +4,6 @@
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
JAVA_HOME = /usr/lib/jvm/java-19-coffeelibs
DEB_BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH)
ifeq ($(DEB_BUILD_ARCH),amd64)
JMODS_PATH = jmods/amd64:${JAVA_HOME}/jmods
else ifeq ($(DEB_BUILD_ARCH),arm64)
JMODS_PATH = jmods/aarch64:${JAVA_HOME}/jmods
endif
%:
dh $@
@@ -24,16 +16,15 @@ override_dh_auto_clean:
override_dh_auto_build:
mkdir resources
ln -s ../common/org.cryptomator.Cryptomator512.png resources/cryptomator.png
$(JAVA_HOME)/bin/jlink \
jlink \
--output runtime \
--module-path "${JMODS_PATH}" \
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--strip-native-commands \
--no-header-files \
--no-man-pages \
--strip-debug \
--compress=2
$(JAVA_HOME)/bin/jpackage \
jpackage \
--type app-image \
--runtime-image runtime \
--input libs \
@@ -43,15 +34,12 @@ override_dh_auto_build:
--name cryptomator \
--vendor "Skymatic GmbH" \
--copyright "(C) 2016 - 2022 Skymatic GmbH" \
--java-options "--enable-preview" \
--java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" \
--java-options "-Xss5m" \
--java-options "-Xmx256m" \
--java-options "-Dfile.encoding=\"utf-8\"" \
--java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\"" \
--java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\"" \
--java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" \
--java-options "-Dcryptomator.p12Path=\"~/.config/Cryptomator/key.p12\"" \
--java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" \
--java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" \
--java-options "-Dcryptomator.showTrayIcon=false" \
-2
View File
@@ -1,7 +1,5 @@
java-options=-Xss5m \
-Xmx256m \
--enable-preview \
--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64 \
-Dfile.encoding=\"utf-8\" \
-Dcryptomator.appVersion=\"${SEMVER_STR}\" \
-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\" \
-2
View File
@@ -10,6 +10,4 @@ java \
-Djdk.gtk.version=2 \
-Xss2m \
-Xmx512m \
--enable-preview \
--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64 \
-m org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator
+1 -4
View File
@@ -45,7 +45,7 @@ cp ../../../target/${MAIN_JAR_GLOB} ../../../target/mods
${JAVA_HOME}/bin/jlink \
--output runtime \
--module-path "${JAVA_HOME}/jmods" \
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--strip-native-commands \
--no-header-files \
--no-man-pages \
@@ -65,8 +65,6 @@ ${JAVA_HOME}/bin/jpackage \
--vendor "${VENDOR}" \
--copyright "(C) ${COPYRIGHT_YEARS} ${VENDOR}" \
--app-version "${VERSION_NO}" \
--java-options "--enable-preview" \
--java-options "--enable-native-access=org.cryptomator.jfuse.mac" \
--java-options "-Xss5m" \
--java-options "-Xmx256m" \
--java-options "-Dfile.encoding=\"utf-8\"" \
@@ -77,7 +75,6 @@ ${JAVA_HOME}/bin/jpackage \
--java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/${APP_NAME}/Plugins\"" \
--java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/${APP_NAME}/settings.json\"" \
--java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/${APP_NAME}/ipc.socket\"" \
--java-options "-Dcryptomator.p12Path=\"~/Library/Application Support/${APP_NAME}/key.p12\"" \
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"${APP_NAME}\"" \
--java-options "-Dcryptomator.showTrayIcon=true" \
--java-options "-Dcryptomator.buildNumber=\"dmg-${REVISION_NO}\"" \
-2
View File
@@ -9,6 +9,4 @@ java \
-Dcryptomator.mountPointsDir="/Volumes" \
-Xss20m \
-Xmx512m \
--enable-preview \
--enable-native-access=org.cryptomator.jfuse.mac \
-m org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator
+1 -3
View File
@@ -9,9 +9,8 @@ SET ABOUT_URL="https://cryptomator.org"
SET UPDATE_URL="https://cryptomator.org/downloads/"
SET HELP_URL="https://cryptomator.org/contact/"
SET MODULE_AND_MAIN_CLASS="org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator"
SET LOOPBACK_ALIAS="cryptomator-vault"
powershell -NoLogo -ExecutionPolicy Unrestricted -Command .\build.ps1^
powershell -NoLogo -NoExit -ExecutionPolicy Unrestricted -Command .\build.ps1^
-AppName %APPNAME%^
-MainJarGlob "%MAIN_JAR_GLOB%"^
-ModuleAndMainClass "%MODULE_AND_MAIN_CLASS%"^
@@ -21,5 +20,4 @@ powershell -NoLogo -ExecutionPolicy Unrestricted -Command .\build.ps1^
-AboutUrl "%ABOUT_URL%"^
-HelpUrl "%HELP_URL%"^
-UpdateUrl "%UPDATE_URL%"^
-LoopbackAlias "%LOOPBACK_ALIAS%"^
-Clean 1
+3 -16
View File
@@ -8,7 +8,6 @@ Param(
[Parameter(Mandatory, HelpMessage="Please provide a help url")][string] $HelpUrl,
[Parameter(Mandatory, HelpMessage="Please provide an update url")][string] $UpdateUrl,
[Parameter(Mandatory, HelpMessage="Please provide an about url")][string] $AboutUrl,
[Parameter(Mandatory, HelpMessage="Please provide an alias for localhost")][string] $LoopbackAlias,
[bool] $clean
)
@@ -51,7 +50,7 @@ if ($clean -and (Test-Path -Path $runtimeImagePath)) {
--verbose `
--output runtime `
--module-path "$Env:JAVA_HOME/jmods" `
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr `
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr `
--strip-native-commands `
--no-header-files `
--no-man-pages `
@@ -75,8 +74,6 @@ if ($clean -and (Test-Path -Path $appPath)) {
--name $AppName `
--vendor $Vendor `
--copyright $copyright `
--java-options "--enable-preview" `
--java-options "--enable-native-access=org.cryptomator.jfuse.win" `
--java-options "-Xss5m" `
--java-options "-Xmx256m" `
--java-options "-Dcryptomator.appVersion=`"$semVerNo`"" `
@@ -86,11 +83,9 @@ if ($clean -and (Test-Path -Path $appPath)) {
--java-options "-Dcryptomator.pluginDir=`"~/AppData/Roaming/$AppName/Plugins`"" `
--java-options "-Dcryptomator.settingsPath=`"~/AppData/Roaming/$AppName/settings.json`"" `
--java-options "-Dcryptomator.ipcSocketPath=`"~/AppData/Roaming/$AppName/ipc.socket`"" `
--java-options "-Dcryptomator.p12Path=`"~/AppData/Roaming/$AppName/key.p12`"" `
--java-options "-Dcryptomator.keychainPath=`"~/AppData/Roaming/$AppName/keychain.json`"" `
--java-options "-Dcryptomator.mountPointsDir=`"~/$AppName`"" `
--java-options "-Dcryptomator.loopbackAlias=`"$LoopbackAlias`"" `
--java-options "-Dcryptomator.integrationsWin.autoStartShellLinkName=`"$AppName`"" `
--java-options "-Dcryptomator.integrationsWin.keychainPaths=`"~/AppData/Roaming/$AppName/keychain.json`"" `
--java-options "-Dcryptomator.showTrayIcon=true" `
--java-options "-Dcryptomator.buildNumber=`"msi-$revisionNo`"" `
--resource-dir resources `
@@ -109,14 +104,6 @@ if ($clean -and (Test-Path -Path $appPath)) {
# patch app dir
Copy-Item "contrib\*" -Destination "$AppName"
attrib -r "$AppName\$AppName.exe"
# patch batch script to set hostfile
$webDAVPatcher = "$AppName\patchWebDAV.bat"
try {
(Get-Content $webDAVPatcher ) -replace '::REPLACE ME', "SET LOOPBACK_ALIAS=`"$LoopbackAlias`"" | Set-Content $webDAVPatcher
} catch {
Write-Host "Failed to set LOOPBACK_ALIAS for patchWebDAV.bat"
exit 1
}
# create .msi
$Env:JP_WIXWIZARD_RESOURCES = "$buildDir\resources"
@@ -153,7 +140,7 @@ $Env:JP_WIXWIZARD_RESOURCES = "$buildDir\resources"
# download Winfsp
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ProgressPreference = 'SilentlyContinue' # disables Invoke-WebRequest's progress bar, which slows down downloads to a few bytes/s
$winfspMsiUrl= (Select-String -Path ".\bundle\resources\winfsp-download.url" -Pattern 'https:.*').Matches.Value
$winfspMsiUrl = "https://github.com/winfsp/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi"
Write-Output "Downloading ${winfspMsiUrl}..."
Invoke-WebRequest $winfspMsiUrl -OutFile ".\bundle\resources\winfsp.msi" # redirects are followed by default
-2
View File
@@ -1,2 +0,0 @@
[InternetShortcut]
URL=https://github.com/winfsp/winfsp/releases/download/v1.12/winfsp-1.12.22301.msi
+1 -1
View File
@@ -1,6 +1,6 @@
@echo off
:: Default values for Cryptomator builds
::REPLACE ME
SET LOOPBACK_ALIAS="cryptomator-vault"
cd %~dp0
powershell -NoLogo -NonInteractive -ExecutionPolicy Unrestricted -Command .\patchWebDAV.ps1^
-2
View File
@@ -9,6 +9,4 @@ java ^
-Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" ^
-Xss20m ^
-Xmx512m ^
--enable-preview `
--enable-native-access=org.cryptomator.jfuse.win `
-m org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator
+11 -16
View File
@@ -23,11 +23,6 @@
<?define JpUpgradeVersionOnlyDetectDowngrade="yes"?>
<?endif?>
<!-- Cryptomator defaults -->
<?define IconFileEncryptedData= "Cryptomator-Vault.ico" ?>
<?define ProgIdContentType= "application/vnd.cryptomator.encrypted" ?>
<?define CloseApplicationTarget= "cryptomator.exe" ?>
<?include $(var.JpConfigDir)/overrides.wxi ?>
<Product
@@ -70,16 +65,16 @@
<CustomAction Id="JpDisallowDowngrade" Error="!(loc.DowngradeErrorMessage)" />
<?endif?>
<?ifndef SkipCryptomatorLegacyCheck ?>
<!-- Block installation if innosetup entry of Cryptomator is found -->
<Property Id="OLDEXEINSTALLER">
<RegistrySearch Id="InnoSetupInstallation" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\Cryptomator_is1" Type="raw" Name="DisplayName" />
</Property>
<!-- TODO: localize -->
<Condition Message="A lower version of [ProductName] is already installed. Uninstall it first and then start the setup again. Setup will now exit.">
<![CDATA[Installed OR NOT OLDEXEINSTALLER]]>
</Condition>
<?endif?>
<!-- Looking for legacy Cryptomator versions-->
<Property Id="OLDEXEINSTALLER">
<RegistrySearch Id="InnoSetupInstallation" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\Cryptomator_is1" Type="raw" Name="DisplayName" />
</Property>
<!-- Block installation if innosetup entry of Cryptomator is found -->
<!-- TODO: localize -->
<Condition Message="A lower version of [ProductName] is already installed. Uninstall it first and then start the setup again. Setup will now exit.">
<![CDATA[Installed OR NOT OLDEXEINSTALLER]]>
</Condition>
<!-- Cryptomator uses UNIX Sockets, which are supported starting with Windows 10 v1803-->
<Property Id="WINDOWSBUILDNUMBER" Secure="yes">
<RegistrySearch Id="BuildNumberSearch" Root="HKLM" Key="SOFTWARE\Microsoft\Windows NT\CurrentVersion" Name="CurrentBuildNumber" Type="raw" />
@@ -91,7 +86,7 @@
<!-- Non-Opening ProgID -->
<DirectoryRef Id="INSTALLDIR">
<Component Win64="yes" Id="nonStartingProgID" >
<File Id="IconFileForEncryptedData" KeyPath="yes" Source="$(env.JP_WIXWIZARD_RESOURCES)\$(var.IconFileEncryptedData)" Name="$(var.IconFileEncryptedData)"></File>
<File Id="IconFileForEncryptedData" KeyPath="yes" Source="$(env.JP_WIXWIZARD_RESOURCES)\$(var.IconFileC9rC9s)" Name="$(var.IconFileC9rC9s)"></File>
<ProgId Id="$(var.JpAppName).Encrypted.1" Description="$(var.JpAppName) Encrypted Data" Icon="IconFileForEncryptedData" IconIndex="0">
<Extension Id="c9r" Advertise="no" ContentType="$(var.ProgIdContentType)">
<MIME ContentType="$(var.ProgIdContentType)" Default="yes"></MIME>
+6 -17
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Stub by design -->
<!-- jPackage Section
<!--
overrides.wxi is a placeholder to set/alter WiX variables referenced from default
main.wxs file.
@@ -30,21 +30,10 @@ Should be defined to enable upgrades and undefined to disable upgrades.
Default value is `yes`.
-->
<!-- Cryptomator Section
<!-- Non-opening ProgID settings-->
<?define IconFileC9rC9s= "Cryptomator-Vault.ico" ?>
<?define ProgIdContentType= "application/vnd.cryptomator.encrypted" ?>
Non-opening ProgID settings:
- IconFileEncryptedData
Full file name of icon file used for encrypted data files. Default is "Cryptomator-Vault.ico"
- ProgIdContentType
Media Type of the encrypted data files. Default is "application/vnd.cryptomator.encrypted"
Close Application settings:
- CloseApplicationTarget
Full name of executable to be checkd in the close application util. Default is "cryptomator.exe"
Legacy Installation settings:
- SkipCryptomatorLegacyCheck
Should be defined to disable checking for the inno setup installation of Cryptomator and undefined, to enable it.
-->
<!-- Close Application util -->
<?define CloseApplicationTarget= "cryptomator.exe" ?>
<Include/>
+26 -41
View File
@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>cryptomator</artifactId>
<version>1.7.0</version>
<version>1.7.0-SNAPSHOT</version>
<name>Cryptomator Desktop App</name>
<organization>
@@ -21,45 +21,42 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.jdk.version>19</project.jdk.version>
<project.jdk.version>17</project.jdk.version>
<!-- Group IDs of jars that need to stay on the class path for now -->
<!-- Check progress on https://github.com/swiesend/secret-service/issues/31 to remove hypfvieh, swiesend and jnr -->
<nonModularGroupIds>com.github.jnr,org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh</nonModularGroupIds>
<nonModularGroupIds>com.github.serceman,com.github.jnr,org.ow2.asm,net.java.dev.jna,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh</nonModularGroupIds>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>2.5.3</cryptomator.cryptofs.version>
<cryptomator.integrations.version>1.2.0-beta2</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.1.2</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.1.2</cryptomator.integrations.mac.version>
<cryptomator.cryptofs.version>2.4.2</cryptomator.cryptofs.version>
<cryptomator.integrations.version>1.1.0</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.1.1</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.1.1</cryptomator.integrations.mac.version>
<cryptomator.integrations.linux.version>1.1.0</cryptomator.integrations.linux.version>
<cryptomator.fuse.version>2.0.0-beta2</cryptomator.fuse.version>
<cryptomator.dokany.version>2.0.0-beta1</cryptomator.dokany.version>
<cryptomator.webdav.version>2.0.0-beta1</cryptomator.webdav.version>
<cryptomator.fuse.version>1.3.4</cryptomator.fuse.version>
<cryptomator.dokany.version>1.3.3</cryptomator.dokany.version>
<cryptomator.webdav.version>1.2.7</cryptomator.webdav.version>
<!-- 3rd party dependencies -->
<javafx.version>18.0.1</javafx.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
<dagger.version>2.44</dagger.version>
<jwt.version>3.19.1</jwt.version>
<easybind.version>2.2</easybind.version>
<guava.version>31.1-jre</guava.version>
<gson.version>2.9.1</gson.version>
<javafx.version>19</javafx.version>
<jwt.version>4.2.1</jwt.version>
<nimbus-jose.version>9.25.4</nimbus-jose.version>
<logback.version>1.4.4</logback.version>
<slf4j.version>2.0.3</slf4j.version>
<tinyoauth2.version>0.5.1</tinyoauth2.version>
<dagger.version>2.41</dagger.version>
<gson.version>2.9.0</gson.version>
<zxcvbn.version>1.7.0</zxcvbn.version>
<slf4j.version>1.7.36</slf4j.version>
<logback.version>1.2.11</logback.version>
<!-- test dependencies -->
<junit.jupiter.version>5.9.1</junit.jupiter.version>
<mockito.version>4.8.0</mockito.version>
<junit.jupiter.version>5.8.1</junit.jupiter.version>
<mockito.version>4.4.0</mockito.version>
<hamcrest.version>2.2</hamcrest.version>
<!-- build-time dependencies -->
<jetbrains.annotations.version>23.0.0</jetbrains.annotations.version>
<dependency-check.version>7.4.0</dependency-check.version>
<jacoco.version>0.8.8</jacoco.version>
<dependency-check.version>7.1.0</dependency-check.version>
<jacoco.version>0.8.7</jacoco.version>
</properties>
<dependencies>
@@ -136,22 +133,12 @@
<version>${commons-lang3.version}</version>
</dependency>
<!-- OAuth/JWT -->
<dependency>
<groupId>io.github.coffeelibs</groupId>
<artifactId>tiny-oauth2-client</artifactId>
<version>${tinyoauth2.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus-jose.version}</version>
</dependency>
<!-- EasyBind -->
<dependency>
@@ -258,7 +245,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.0</version>
<version>3.2.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -268,7 +255,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<version>3.0.0-M5</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
@@ -278,7 +265,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<version>3.2.2</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
@@ -308,14 +295,13 @@
<compilerArgs>
<arg>-Adagger.fastInit=enabled</arg>
<arg>-Adagger.formatGeneratedSource=enabled</arg>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<version>3.0.0</version>
<executions>
<execution>
<id>compile-light-theme</id>
@@ -367,7 +353,6 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<!-- sort jars into two buckets (classpath and modulepath). exclude openjfx, which gets jlinked separately -->
<execution>
<id>copy-mods</id>
<phase>prepare-package</phase>
@@ -377,7 +362,7 @@
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/mods</outputDirectory>
<excludeGroupIds>org.openjfx,${nonModularGroupIds}</excludeGroupIds>
<excludeGroupIds>${nonModularGroupIds}</excludeGroupIds>
</configuration>
</execution>
<execution>
+33 -15
View File
@@ -1,42 +1,60 @@
import ch.qos.logback.classic.spi.Configurator;
import org.cryptomator.integrations.tray.TrayMenuController;
import org.cryptomator.logging.LogbackConfiguratorFactory;
import org.cryptomator.ui.traymenu.AwtTrayMenuController;
open module org.cryptomator.desktop {
module org.cryptomator.desktop {
requires static org.jetbrains.annotations;
requires org.cryptomator.cryptolib;
requires org.cryptomator.cryptofs;
requires org.cryptomator.frontend.dokany;
requires org.cryptomator.frontend.fuse;
requires org.cryptomator.frontend.webdav;
requires org.cryptomator.integrations.api;
// jdk:
requires java.desktop;
requires java.net.http;
requires javafx.base;
requires javafx.graphics;
requires javafx.controls;
requires javafx.fxml;
requires jdk.crypto.ec;
// 3rd party:
requires ch.qos.logback.classic;
requires ch.qos.logback.core;
requires com.auth0.jwt;
requires com.tobiasdiez.easybind;
requires com.google.common;
requires com.google.gson;
requires com.nimbusds.jose.jwt;
requires com.nulabinc.zxcvbn;
requires com.tobiasdiez.easybind;
requires dagger;
requires io.github.coffeelibs.tinyoauth2client;
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;
exports org.cryptomator.ui.traymenu to org.cryptomator.integrations.api;
provides TrayMenuController with AwtTrayMenuController;
provides Configurator with LogbackConfiguratorFactory;
opens org.cryptomator.common.settings to com.google.gson;
opens org.cryptomator.launcher to javafx.graphics;
opens org.cryptomator.common to javafx.fxml;
opens org.cryptomator.common.vaults to javafx.fxml;
opens org.cryptomator.ui.addvaultwizard to javafx.fxml;
opens org.cryptomator.ui.changepassword to javafx.fxml;
opens org.cryptomator.ui.common to javafx.fxml;
opens org.cryptomator.ui.controls to javafx.fxml;
opens org.cryptomator.ui.forgetPassword to javafx.fxml;
opens org.cryptomator.ui.fxapp to javafx.fxml;
opens org.cryptomator.ui.health to javafx.fxml;
opens org.cryptomator.ui.keyloading.masterkeyfile to javafx.fxml;
opens org.cryptomator.ui.lock to javafx.fxml;
opens org.cryptomator.ui.mainwindow to javafx.fxml;
opens org.cryptomator.ui.migration to javafx.fxml;
opens org.cryptomator.ui.preferences to javafx.fxml;
opens org.cryptomator.ui.quit to javafx.fxml;
opens org.cryptomator.ui.recoverykey to javafx.fxml;
opens org.cryptomator.ui.removevault to javafx.fxml;
opens org.cryptomator.ui.stats to javafx.fxml;
opens org.cryptomator.ui.unlock to javafx.fxml;
opens org.cryptomator.ui.vaultoptions to javafx.fxml;
opens org.cryptomator.ui.wrongfilealert to javafx.fxml;
}
@@ -10,7 +10,6 @@ import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
@@ -29,18 +28,6 @@ public final class CatchingExecutors {
super(corePoolSize, threadFactory);
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
Runnable oneShot = () -> this.execute(command);
return super.scheduleAtFixedRate(oneShot, initialDelay, period, unit);
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
Runnable oneShot = () -> this.execute(command);
return super.scheduleWithFixedDelay(oneShot, initialDelay, delay, unit);
}
@Override
protected void afterExecute(Runnable runnable, Throwable throwable) {
super.afterExecute(runnable, throwable);
@@ -66,7 +53,7 @@ public final class CatchingExecutors {
callHandler(Thread.currentThread(), throwable);
} else if (runnable instanceof Task<?> t) {
afterExecuteTask(t);
} else if (runnable instanceof Future<?> f) {
} else if (runnable instanceof Future<?> f && f.isDone()) {
afterExecuteFuture(f);
}
//Errors in this method are delegated to the UncaughtExceptionHandler of the current thread
@@ -90,16 +77,11 @@ public final class CatchingExecutors {
}
private static void afterExecuteFuture(Future<?> future) {
if (future instanceof ScheduledFuture<?> && !future.isDone()) {
//we assume that this must be a repeated ScheduledFutureTask, where the done-status is only set when not executed anymore
//see also https://github.com/cryptomator/cryptomator/pull/2422
return;
}
assert future.isDone();
try {
future.get();
} catch (CancellationException ce) {
//Ignore
callHandler(Thread.currentThread(), ce);
} catch (ExecutionException ee) {
callHandler(Thread.currentThread(), ee.getCause());
} catch (InterruptedException ie) {
@@ -10,18 +10,19 @@ import dagger.Module;
import dagger.Provides;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.keychain.KeychainModule;
import org.cryptomator.common.mount.MountModule;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.common.vaults.VaultComponent;
import org.cryptomator.common.vaults.VaultListModule;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Singleton;
import javafx.beans.value.ObservableValue;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import java.net.InetSocketAddress;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@@ -32,7 +33,7 @@ import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class, MountModule.class})
@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class})
public abstract class CommonsModule {
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
@@ -40,12 +41,6 @@ public abstract class CommonsModule {
private static final int NUM_CORE_BG_THREADS = 6;
private static final long BG_THREAD_KEEPALIVE_SECONDS = 60l;
@Provides
@Singleton
static Environment provideEnvironment() {
return Environment.getInstance();
}
@SuppressWarnings("SpellCheckingInspection")
@Provides
@Singleton
@@ -129,11 +124,20 @@ public abstract class CommonsModule {
@Provides
@Singleton
static ObservableValue<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
return settings.port().map(port -> {
static Binding<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
return Bindings.createObjectBinding(() -> {
String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
return InetSocketAddress.createUnresolved(host, settings.port().intValue());
});
}, settings.port());
}
@Provides
@Singleton
static WebDavServer provideWebDavServer(Binding<InetSocketAddress> serverSocketAddressBinding) {
WebDavServer server = WebDavServer.create();
// no need to unsubscribe eventually, because server is a singleton
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
return server;
}
}
@@ -5,6 +5,8 @@ import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -15,57 +17,31 @@ import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@Singleton
public class Environment {
private static final Logger LOG = LoggerFactory.getLogger(Environment.class);
private static final Path RELATIVE_HOME_DIR = Paths.get("~");
private static final char PATH_LIST_SEP = ':';
private static final int DEFAULT_MIN_PW_LENGTH = 8;
private static final String SETTINGS_PATH_PROP_NAME = "cryptomator.settingsPath";
private static final String IPC_SOCKET_PATH_PROP_NAME = "cryptomator.ipcSocketPath";
private static final String KEYCHAIN_PATHS_PROP_NAME = "cryptomator.integrationsWin.keychainPaths";
private static final String P12_PATH_PROP_NAME = "cryptomator.p12Path";
private static final String LOG_DIR_PROP_NAME = "cryptomator.logDir";
private static final String LOOPBACK_ALIAS_PROP_NAME = "cryptomator.loopbackAlias";
private static final String MOUNTPOINT_DIR_PROP_NAME = "cryptomator.mountPointsDir";
private static final String MIN_PW_LENGTH_PROP_NAME = "cryptomator.minPwLength";
private static final String APP_VERSION_PROP_NAME = "cryptomator.appVersion";
private static final String BUILD_NUMBER_PROP_NAME = "cryptomator.buildNumber";
private static final String PLUGIN_DIR_PROP_NAME = "cryptomator.pluginDir";
private static final String TRAY_ICON_PROP_NAME = "cryptomator.showTrayIcon";
private Environment() {}
public void log() {
LOG.info("user.home: {}", System.getProperty("user.home"));
LOG.info("java.library.path: {}", System.getProperty("java.library.path"));
LOG.info("user.language: {}", System.getProperty("user.language"));
LOG.info("user.region: {}", System.getProperty("user.region"));
LOG.info("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));
logCryptomatorSystemProperty(SETTINGS_PATH_PROP_NAME);
logCryptomatorSystemProperty(IPC_SOCKET_PATH_PROP_NAME);
logCryptomatorSystemProperty(KEYCHAIN_PATHS_PROP_NAME);
logCryptomatorSystemProperty(LOG_DIR_PROP_NAME);
logCryptomatorSystemProperty(LOOPBACK_ALIAS_PROP_NAME);
logCryptomatorSystemProperty(PLUGIN_DIR_PROP_NAME);
logCryptomatorSystemProperty(MOUNTPOINT_DIR_PROP_NAME);
logCryptomatorSystemProperty(MIN_PW_LENGTH_PROP_NAME);
logCryptomatorSystemProperty(APP_VERSION_PROP_NAME);
logCryptomatorSystemProperty(BUILD_NUMBER_PROP_NAME);
logCryptomatorSystemProperty(TRAY_ICON_PROP_NAME);
logCryptomatorSystemProperty(P12_PATH_PROP_NAME);
}
public static Environment getInstance() {
final class Holder {
private static final Environment INSTANCE = new Environment();
}
return Holder.INSTANCE;
}
private void logCryptomatorSystemProperty(String propertyName) {
LOG.info("{}: {}", propertyName, System.getProperty(propertyName));
@Inject
public Environment() {
LOG.debug("user.home: {}", System.getProperty("user.home"));
LOG.debug("java.library.path: {}", System.getProperty("java.library.path"));
LOG.debug("user.language: {}", System.getProperty("user.language"));
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.ipcSocketPath: {}", System.getProperty("cryptomator.ipcSocketPath"));
LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath"));
LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir"));
LOG.debug("cryptomator.pluginDir: {}", System.getProperty("cryptomator.pluginDir"));
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
LOG.debug("cryptomator.appVersion: {}", System.getProperty("cryptomator.appVersion"));
LOG.debug("cryptomator.buildNumber: {}", System.getProperty("cryptomator.buildNumber"));
LOG.debug("cryptomator.showTrayIcon: {}", System.getProperty("cryptomator.showTrayIcon"));
}
public boolean useCustomLogbackConfig() {
@@ -73,56 +49,52 @@ public class Environment {
}
public Stream<Path> getSettingsPath() {
return getPaths(SETTINGS_PATH_PROP_NAME);
}
public Stream<Path> getP12Path() {
return getPaths(P12_PATH_PROP_NAME);
return getPaths("cryptomator.settingsPath");
}
public Stream<Path> ipcSocketPath() {
return getPaths(IPC_SOCKET_PATH_PROP_NAME);
return getPaths("cryptomator.ipcSocketPath");
}
public Stream<Path> getKeychainPath() {
return getPaths(KEYCHAIN_PATHS_PROP_NAME);
return getPaths("cryptomator.keychainPath");
}
public Optional<Path> getLogDir() {
return getPath(LOG_DIR_PROP_NAME).map(this::replaceHomeDir);
}
public Optional<String> getLoopbackAlias() {
return Optional.ofNullable(System.getProperty(LOOPBACK_ALIAS_PROP_NAME));
return getPath("cryptomator.logDir").map(this::replaceHomeDir);
}
public Optional<Path> getPluginDir() {
return getPath(PLUGIN_DIR_PROP_NAME).map(this::replaceHomeDir);
return getPath("cryptomator.pluginDir").map(this::replaceHomeDir);
}
public Optional<Path> getMountPointsDir() {
return getPath(MOUNTPOINT_DIR_PROP_NAME).map(this::replaceHomeDir);
return getPath("cryptomator.mountPointsDir").map(this::replaceHomeDir);
}
/**
* Returns the app version defined in the {@value APP_VERSION_PROP_NAME} property or returns "SNAPSHOT".
*
* @return App version or "SNAPSHOT", if undefined
*/
public String getAppVersion() {
return System.getProperty(APP_VERSION_PROP_NAME, "SNAPSHOT");
public Optional<String> getAppVersion() {
return Optional.ofNullable(System.getProperty("cryptomator.appVersion"));
}
public Optional<String> getBuildNumber() {
return Optional.ofNullable(System.getProperty(BUILD_NUMBER_PROP_NAME));
return Optional.ofNullable(System.getProperty("cryptomator.buildNumber"));
}
public int getMinPwLength() {
return Integer.getInteger(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH);
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
}
public boolean showTrayIcon() {
return Boolean.getBoolean(TRAY_ICON_PROP_NAME);
return Boolean.getBoolean("cryptomator.showTrayIcon");
}
private int getInt(String propertyName, int defaultValue) {
String value = System.getProperty(propertyName);
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) { // includes "null" values
return defaultValue;
}
}
private Optional<Path> getPath(String propertyName) {
@@ -10,7 +10,6 @@ import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import java.util.Optional;
@Singleton
@@ -19,7 +18,7 @@ public class LicenseHolder {
private final Settings settings;
private final LicenseChecker licenseChecker;
private final ObjectProperty<DecodedJWT> validJwtClaims;
private final ObservableValue<String> licenseSubject;
private final StringBinding licenseSubject;
private final BooleanBinding validLicenseProperty;
@Inject
@@ -27,7 +26,7 @@ public class LicenseHolder {
this.settings = settings;
this.licenseChecker = licenseChecker;
this.validJwtClaims = new SimpleObjectProperty<>();
this.licenseSubject = validJwtClaims.map(DecodedJWT::getSubject);
this.licenseSubject = Bindings.createStringBinding(this::getLicenseSubject, validJwtClaims);
this.validLicenseProperty = validJwtClaims.isNotNull();
Optional<DecodedJWT> claims = licenseChecker.check(settings.licenseKey().get());
@@ -56,12 +55,17 @@ public class LicenseHolder {
}
}
public ObservableValue<String> licenseSubjectProperty() {
public StringBinding licenseSubjectProperty() {
return licenseSubject;
}
public String getLicenseSubject() {
return licenseSubject.getValue();
DecodedJWT claims = validJwtClaims.get();
if (claims != null) {
return claims.getSubject();
} else {
return null;
}
}
public BooleanBinding validLicenseProperty() {
@@ -23,14 +23,11 @@ public class KeychainModule {
@Singleton
static ObjectExpression<KeychainAccessProvider> provideKeychainAccessProvider(Settings settings, List<KeychainAccessProvider> providers) {
return Bindings.createObjectBinding(() -> {
if (!settings.useKeychain().get()) {
return null;
}
var selectedProviderClass = settings.keychainProvider().get();
var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny();
var fallbackProvider = providers.stream().findFirst().orElse(null);
return selectedProvider.orElse(fallbackProvider);
}, settings.keychainProvider(), settings.useKeychain());
}, settings.keychainProvider());
}
}
@@ -1,30 +0,0 @@
package org.cryptomator.common.mount;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.integrations.mount.MountService;
import javax.inject.Singleton;
import javafx.beans.value.ObservableValue;
import java.util.List;
@Module
public class MountModule {
@Provides
@Singleton
static List<MountService> provideSupportedMountServices() {
return MountService.get().toList();
}
@Provides
@Singleton
static ObservableValue<MountService> provideMountService(Settings settings, List<MountService> serviceImpls) {
return settings.mountService().map(desiredServiceImpl -> {
var fallbackProvider = serviceImpls.stream().findFirst().orElse(null);
return serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(desiredServiceImpl)).findAny().orElse(fallbackProvider);
});
}
}
@@ -1,70 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common.mount;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.SystemUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;
@Singleton
public final class WindowsDriveLetters {
private static final Set<Path> A_TO_Z;
static {
var sortedSet = new TreeSet<Path>();
IntStream.rangeClosed('A', 'Z').mapToObj(i -> Path.of((char) i + ":\\")).forEach(sortedSet::add);
A_TO_Z = Collections.unmodifiableSet(sortedSet);
}
@Inject
public WindowsDriveLetters() {
}
public Set<Path> getAll() {
return A_TO_Z;
}
public Set<Path> getOccupied() {
if (!SystemUtils.IS_OS_WINDOWS) {
return Set.of();
} else {
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
return StreamSupport.stream(rootDirs.spliterator(), false).collect(Collectors.toUnmodifiableSet());
}
}
public Set<Path> getAvailable() {
return Sets.difference(getAll(), getOccupied());
}
/**
* Skips A and B and only returns them if all others are occupied.
*
* @return an Optional containing either the letter of a free drive letter or empty, if none is available
*/
public Optional<Path> getFirstDesiredAvailable() {
var availableDriveLetters = getAvailable();
var optString = availableDriveLetters.stream().filter(this::notAOrB).findFirst();
return optString.or(() -> availableDriveLetters.stream().findFirst());
}
private boolean notAOrB(Path driveLetter) {
return !(Path.of("A:\\").equals(driveLetter) || Path.of("B:\\").equals(driveLetter));
}
}
@@ -0,0 +1,29 @@
package org.cryptomator.common.mountpoint;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.common.vaults.WindowsDriveLetters;
import javax.inject.Inject;
import java.nio.file.Path;
import java.util.Optional;
class AvailableDriveLetterChooser implements MountPointChooser {
private final WindowsDriveLetters windowsDriveLetters;
@Inject
public AvailableDriveLetterChooser(WindowsDriveLetters windowsDriveLetters) {
this.windowsDriveLetters = windowsDriveLetters;
}
@Override
public boolean isApplicable(Volume caller) {
return SystemUtils.IS_OS_WINDOWS;
}
@Override
public Optional<Path> chooseMountPoint(Volume caller) {
return this.windowsDriveLetters.getDesiredAvailableDriveLetterPath();
}
}
@@ -0,0 +1,42 @@
package org.cryptomator.common.mountpoint;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Volume;
import javax.inject.Inject;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
class CustomDriveLetterChooser implements MountPointChooser {
private final VaultSettings vaultSettings;
@Inject
public CustomDriveLetterChooser(VaultSettings vaultSettings) {
this.vaultSettings = vaultSettings;
}
@Override
public boolean isApplicable(Volume caller) {
return SystemUtils.IS_OS_WINDOWS;
}
@Override
public Optional<Path> chooseMountPoint(Volume caller) {
return this.vaultSettings.getWinDriveLetter().map(letter -> letter.charAt(0) + ":\\").map(Paths::get);
}
@Override
public boolean prepare(Volume caller, Path driveLetter) throws InvalidMountPointException {
if (!Files.notExists(driveLetter, LinkOption.NOFOLLOW_LINKS)) {
//Drive already exists OR can't be determined
throw new InvalidMountPointException(new FileAlreadyExistsException(driveLetter.toString()));
}
return false;
}
}
@@ -0,0 +1,150 @@
package org.cryptomator.common.mountpoint;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Volume;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Optional;
class CustomMountPointChooser implements MountPointChooser {
private static final String HIDEAWAY_PREFIX = ".~$";
private static final String HIDEAWAY_SUFFIX = ".tmp";
private static final String WIN_HIDDEN = "dos:hidden";
private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class);
private final VaultSettings vaultSettings;
@Inject
public CustomMountPointChooser(VaultSettings vaultSettings) {
this.vaultSettings = vaultSettings;
}
@Override
public boolean isApplicable(Volume caller) {
return caller.getImplementationType() != VolumeImpl.WEBDAV;
}
@Override
public Optional<Path> chooseMountPoint(Volume caller) {
//VaultSettings#getCustomMountPath already checks whether the saved custom mountpoint should be used
return this.vaultSettings.getCustomMountPath().map(Paths::get);
}
@Override
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
return switch (caller.getMountPointRequirement()) {
case PARENT_NO_MOUNT_POINT -> {
prepareParentNoMountPoint(mountPoint);
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
yield true;
}
case EMPTY_MOUNT_POINT -> {
prepareEmptyMountPoint(mountPoint);
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
yield false;
}
case NONE, UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT -> {
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
}
};
}
//This is case on Windows when using FUSE
//See https://github.com/billziss-gh/winfsp/issues/320
void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException {
Path hideaway = getHideaway(mountPoint);
var mpExists = Files.exists(mountPoint, LinkOption.NOFOLLOW_LINKS);
var hideExists = Files.exists(hideaway, LinkOption.NOFOLLOW_LINKS);
//TODO: possible improvement by just deleting an _empty_ hideaway
if (mpExists && hideExists) { //both resources exist (whatever type)
throw new InvalidMountPointException(new FileAlreadyExistsException(hideaway.toString()));
} else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist
throw new InvalidMountPointException(new NoSuchFileException(mountPoint.toString()));
} else if (!mpExists) { //only hideaway exists
checkIsDirectory(hideaway);
LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint);
try {
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS);
}
} catch (IOException e) {
throw new InvalidMountPointException(e);
}
} else { //only mountpoint exists
try {
checkIsDirectory(mountPoint);
checkIsEmpty(mountPoint);
Files.move(mountPoint, hideaway);
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS);
}
} catch (IOException e) {
throw new InvalidMountPointException(e);
}
}
}
private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException {
//This is the case for Windows when using Dokany and for Linux and Mac
checkIsDirectory(mountPoint);
try {
checkIsEmpty(mountPoint);
} catch (IOException exception) {
throw new InvalidMountPointException("IOException while checking folder content", exception);
}
}
@Override
public void cleanup(Volume caller, Path mountPoint) {
if (caller.getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT) {
Path hideaway = getHideaway(mountPoint);
try {
Files.move(hideaway, mountPoint);
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(mountPoint, WIN_HIDDEN, false);
}
} catch (IOException e) {
LOG.error("Unable to clean up mountpoint {} for Winfsp mounting.", mountPoint, e);
}
}
}
private void checkIsDirectory(Path toCheck) throws InvalidMountPointException {
if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) {
throw new InvalidMountPointException(new NotDirectoryException(toCheck.toString()));
}
}
private void checkIsEmpty(Path toCheck) throws InvalidMountPointException, IOException {
try (var dirStream = Files.list(toCheck)) {
if (dirStream.findFirst().isPresent()) {
throw new InvalidMountPointException(new DirectoryNotEmptyException(toCheck.toString()));
}
}
}
//visible for testing
Path getHideaway(Path mountPoint) {
return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX);
}
}
@@ -0,0 +1,16 @@
package org.cryptomator.common.mountpoint;
public class InvalidMountPointException extends Exception {
public InvalidMountPointException(String message) {
super(message);
}
public InvalidMountPointException(Throwable cause) {
super(cause);
}
public InvalidMountPointException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,42 @@
package org.cryptomator.common.mountpoint;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Volume;
import javax.inject.Inject;
import java.nio.file.Path;
import java.util.Optional;
class MacVolumeMountChooser implements MountPointChooser {
private static final Path VOLUME_PATH = Path.of("/Volumes");
private final VaultSettings vaultSettings;
private final MountPointHelper helper;
@Inject
public MacVolumeMountChooser(VaultSettings vaultSettings, MountPointHelper helper) {
this.vaultSettings = vaultSettings;
this.helper = helper;
}
@Override
public boolean isApplicable(Volume caller) {
return SystemUtils.IS_OS_MAC;
}
@Override
public Optional<Path> chooseMountPoint(Volume caller) {
return Optional.of(helper.chooseTemporaryMountPoint(vaultSettings, VOLUME_PATH));
}
@Override
public boolean prepare(Volume caller, Path mountPoint) {
// https://github.com/osxfuse/osxfuse/issues/306#issuecomment-245114592:
// In order to allow non-admin users to mount FUSE volumes in `/Volumes`,
// starting with version 3.5.0, FUSE will create non-existent mount points automatically.
// Therefore we don't need to prepare anything.
return false;
}
}
@@ -0,0 +1,138 @@
package org.cryptomator.common.mountpoint;
import dagger.multibindings.IntKey;
import org.cryptomator.common.vaults.Volume;
import java.nio.file.Path;
import java.util.Optional;
import java.util.SortedSet;
/**
* Base interface for the Mountpoint-Choosing-Operation that results in the choice and
* preparation of a mountpoint or an exception otherwise.<br>
* <p>All <i>MountPointChoosers (MPCs)</i> need to implement this class and must be added to
* the pool of possible MPCs by the {@link MountPointChooserModule MountPointChooserModule.}
* The MountPointChooserModule will sort them according to their {@link IntKey IntKey priority.}
* The priority must be defined by the developer to reflect a useful execution order.<br>
* A specific priority <b>must not</b> be assigned to more than one MPC at a time;
* the result of having two MPCs with equal priority is undefined.
*
* <p>MPCs are executed by a {@link Volume} in descending order of their priority
* (higher priorities are tried first) to find and prepare a suitable mountpoint for the volume.
* The volume has access to a {@link SortedSet} of MPCs in this specific order,
* that is provided by the Module. The Set contains all available Choosers, even if they
* are not {@link #isApplicable(Volume) applicable} for the Vault/Volume. The Volume must
* check whether a MPC is applicable by invoking {@code #isApplicable(Volume)} on it
* <i>before</i> calling {@code #chooseMountPoint(Volume)}.
*
* <p>At execution of a MPC {@link #chooseMountPoint(Volume)} is called to choose a mountpoint
* according to the MPC's <i>strategy.</i> The <i>strategy</i> can involve reading configs,
* searching the filesystem, processing user-input or similar operations.
* If {@code #chooseMountPoint(Volume)} returns a non-null path (everything but
* {@linkplain Optional#empty()}) the MPC's {@link #prepare(Volume, Path)} method is called and the
* MountPoint is verified and/or prepared. In this case <i>no other MPC's will be called for
* this volume, even if {@code #prepare(Volume, Path)} fails.</i>
*
* <p>If {@code #chooseMountPoint(Volume)} yields no result, the next MPC is executed
* <i>without</i> first calling the {@code #prepare(Volume, Path)} method of the current MPC.
* This is repeated until<br>
* <ul>
* <li><b>either</b> a mountpoint is returned by {@code #chooseMountPoint(Volume)}
* and {@code #prepare(Volume, Path)} succeeds or fails, ending the entire operation</li>
* <li><b>or</b> no MPC remains and an {@link InvalidMountPointException} is thrown.</li>
* </ul>
* If the {@code #prepare(Volume, Path)} method of a MPC fails, the entire
* Mountpoint-Choosing-Operation is aborted and the method should do all necessary cleanup
* before throwing the exception.
* If the preparation succeeds {@link #cleanup(Volume, Path)} can be used after unmount to do any
* remaining cleanup.
*/
public interface MountPointChooser {
/**
* Called by the {@link Volume} to determine whether this MountPointChooser is
* applicable for mounting the Vault/Volume, especially with regard to the
* current system configuration and particularities of the Volume type.
*
* <p>Developers should override this method to check for system configurations
* that are unsuitable for this MPC.
*
* @param caller The Volume that is calling the method to determine applicability of the MPC
* @return a boolean flag; true if applicable, else false.
* @see #chooseMountPoint(Volume)
*/
boolean isApplicable(Volume caller);
/**
* Called by a {@link Volume} to choose a mountpoint according to the
* MountPointChoosers strategy.
*
* <p>This method must only be called for MPCs that were deemed
* {@link #isApplicable(Volume) applicable} by the {@link Volume Volume.}
* Developers should override this method to find or extract a mountpoint for
* the volume <b>without</b> preparing it. Preparation should be done by
* {@link #prepare(Volume, Path)} instead.
* Exceptions in this method should be handled gracefully and result in returning
* {@link Optional#empty()} instead of throwing an exception.
*
* @param caller The Volume that is calling the method to choose a mountpoint
* @return the chosen path or {@link Optional#empty()} if an exception occurred
* or no mountpoint could be found.
* @see #isApplicable(Volume)
* @see #prepare(Volume, Path)
*/
Optional<Path> chooseMountPoint(Volume caller);
/**
* Called by a {@link Volume} to prepare and/or verify the chosen mountpoint.<br>
* This method is only called if the {@link #chooseMountPoint(Volume)} method
* of the same MountPointChooser returned a path.
*
* <p>Developers should override this method to prepare the mountpoint for
* the volume and check for any obstacles that could hinder the mount operation.
* The mountpoint is deemed "prepared" if it can be used to mount a volume
* without any further filesystem actions or user interaction. If this is not possible,
* this method should fail. In other words: This method should not return without
* either failing or finalizing the preparation of the mountpoint.
* Generally speaking exceptions should be wrapped as
* {@link InvalidMountPointException} to allow efficient handling by the caller.
*
* <p>Often the preparation of a mountpoint involves creating files or others
* actions that require cleaning up after the volume is unmounted.
* In this case developers should override the {@link #cleanup(Volume, Path)}
* method and return {@code true} to the volume to indicate that the
* {@code #cleanup} method of this MPC should be called after unmount.
*
* <p><b>Please note:</b> If this method fails the entire
* Mountpoint-Choosing-Operation is aborted without calling
* {@link #cleanup(Volume, Path)} or any other MPCs. Therefore this method should
* do all necessary cleanup before throwing the exception.
*
* @param caller The Volume that is calling the method to prepare a mountpoint
* @param mountPoint the mountpoint chosen by {@link #chooseMountPoint(Volume)}
* @return a boolean flag; true if cleanup is needed, false otherwise
* @throws InvalidMountPointException if the preparation fails
* @see #chooseMountPoint(Volume)
* @see #cleanup(Volume, Path)
*/
default boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
return false; //NO-OP
}
/**
* Called by a {@link Volume} to do any cleanup needed after unmount.
*
* <p>This method is only called if the {@link #prepare(Volume, Path)} method
* of the same MountPointChooser returned {@code true}. Typically developers want to
* delete any files created prior to mount or do similar tasks.<br>
* Exceptions in this method should be handled gracefully.
*
* @param caller The Volume that is calling the method to cleanup the prepared mountpoint
* @param mountPoint the mountpoint that was prepared by {@link #prepare(Volume, Path)}
* @see #prepare(Volume, Path)
*/
default void cleanup(Volume caller, Path mountPoint) {
//NO-OP
}
}
@@ -0,0 +1,64 @@
package org.cryptomator.common.mountpoint;
import com.google.common.collect.Iterables;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntKey;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.PerVault;
import javax.inject.Named;
import java.util.Comparator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Dagger-Module for {@link MountPointChooser MountPointChoosers.}<br>
* See there for additional information.
*
* @see MountPointChooser
*/
@Module
public abstract class MountPointChooserModule {
@Binds
@IntoMap
@IntKey(1000)
@PerVault
public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser);
@Binds
@IntoMap
@IntKey(900)
@PerVault
public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser);
@Binds
@IntoMap
@IntKey(800)
@PerVault
public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);
@Binds
@IntoMap
@IntKey(101)
@PerVault
public abstract MountPointChooser bindMacVolumeMountChooser(MacVolumeMountChooser chooser);
@Binds
@IntoMap
@IntKey(100)
@PerVault
public abstract MountPointChooser bindTemporaryMountPointChooser(TemporaryMountPointChooser chooser);
@Provides
@PerVault
@Named("orderedMountPointChoosers")
public static Iterable<MountPointChooser> provideOrderedMountPointChoosers(Map<Integer, MountPointChooser> choosers) {
SortedMap<Integer, MountPointChooser> sortedChoosers = new TreeMap<>(Comparator.reverseOrder());
sortedChoosers.putAll(choosers);
return Iterables.unmodifiableIterable(sortedChoosers.values());
}
}
@@ -0,0 +1,122 @@
package org.cryptomator.common.mountpoint;
import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.VaultSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Optional;
@Singleton
class MountPointHelper {
public static Logger LOG = LoggerFactory.getLogger(MountPointHelper.class);
private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
private final Optional<Path> tmpMountPointDir;
private volatile boolean unmountDebrisCleared = false;
@Inject
public MountPointHelper(Environment env) {
this.tmpMountPointDir = env.getMountPointsDir();
}
public Path chooseTemporaryMountPoint(VaultSettings vaultSettings, Path parentDir) {
String basename = vaultSettings.mountName().get();
//regular
Path mountPoint = parentDir.resolve(basename);
if (Files.notExists(mountPoint)) {
return mountPoint;
}
//with id
mountPoint = parentDir.resolve(basename + " (" + vaultSettings.getId() + ")");
if (Files.notExists(mountPoint)) {
return mountPoint;
}
//with id and count
for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
mountPoint = parentDir.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
if (Files.notExists(mountPoint)) {
return mountPoint;
}
}
LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parentDir, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
return null;
}
public synchronized void clearIrregularUnmountDebrisIfNeeded() {
if (unmountDebrisCleared || tmpMountPointDir.isEmpty()) {
return; // nothing to do
}
if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) {
clearIrregularUnmountDebris(tmpMountPointDir.get());
}
unmountDebrisCleared = true;
}
private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {
IOException cleanupFailed = new IOException("Cleanup failed");
try (var ds = Files.newDirectoryStream(dirContainingMountPoints)) {
LOG.debug("Performing cleanup of mountpoint dir {}.", dirContainingMountPoints);
for (Path p : ds) {
try {
var attr = Files.readAttributes(p, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
if (attr.isOther() && attr.isDirectory()) { // yes, this is possible with windows junction points -.-
Files.delete(p);
} else if (attr.isDirectory()) {
deleteEmptyDir(p);
} else if (attr.isSymbolicLink()) {
deleteDeadLink(p);
} else {
LOG.debug("Found non-directory element in mountpoint dir: {}", p);
}
} catch (IOException e) {
cleanupFailed.addSuppressed(e);
}
}
if (cleanupFailed.getSuppressed().length > 0) {
throw cleanupFailed;
}
} catch (IOException e) {
LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e);
} finally {
unmountDebrisCleared = true;
}
}
private void deleteEmptyDir(Path dir) throws IOException {
assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
try {
ensureIsEmpty(dir);
Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents)
} catch (DirectoryNotEmptyException e) {
LOG.info("Found non-empty directory in mountpoint dir: {}", dir);
}
}
private void deleteDeadLink(Path symlink) throws IOException {
assert Files.isSymbolicLink(symlink);
if (Files.notExists(symlink)) { // following link: target does not exist
Files.delete(symlink);
}
}
private void ensureIsEmpty(Path dir) throws IOException {
try (var ds = Files.newDirectoryStream(dir)) {
if (ds.iterator().hasNext()){
throw new DirectoryNotEmptyException(dir.toString());
}
}
}
}
@@ -0,0 +1,87 @@
package org.cryptomator.common.mountpoint;
import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Volume;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
class TemporaryMountPointChooser implements MountPointChooser {
private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
private final VaultSettings vaultSettings;
private final Environment environment;
private final MountPointHelper helper;
@Inject
public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, MountPointHelper helper) {
this.vaultSettings = vaultSettings;
this.environment = environment;
this.helper = helper;
}
@Override
public boolean isApplicable(Volume caller) {
if (this.environment.getMountPointsDir().isEmpty()) {
LOG.warn("\"cryptomator.mountPointsDir\" is not set to a valid path!");
return false;
}
return true;
}
@Override
public Optional<Path> chooseMountPoint(Volume caller) {
assert environment.getMountPointsDir().isPresent();
//clean leftovers of not-regularly unmounted vaults
//see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061
helper.clearIrregularUnmountDebrisIfNeeded();
return this.environment.getMountPointsDir().map(dir -> this.helper.chooseTemporaryMountPoint(this.vaultSettings, dir));
}
@Override
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
try {
switch (caller.getMountPointRequirement()) {
case PARENT_NO_MOUNT_POINT -> {
Files.createDirectories(mountPoint.getParent());
LOG.debug("Successfully created folder for mount point: {}", mountPoint);
return false;
}
case EMPTY_MOUNT_POINT -> {
Files.createDirectories(mountPoint);
LOG.debug("Successfully created mount point: {}", mountPoint);
return true;
}
case NONE -> {
//Requirement "NONE" doesn't make any sense here.
//No need to prepare/verify a Mountpoint without requiring one...
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
}
default -> {
//Currently the case for "UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT"
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
}
}
} catch (IOException exception) {
throw new InvalidMountPointException("IOException while preparing mountpoint", exception);
}
}
@Override
public void cleanup(Volume caller, Path mountPoint) {
try {
Files.delete(mountPoint);
LOG.debug("Successfully deleted mount point: {}", mountPoint);
} catch (IOException e) {
LOG.warn("Could not delete mount point: {}", e.getMessage());
}
}
}
@@ -1,99 +0,0 @@
package org.cryptomator.common.settings;
import com.google.common.base.Preconditions;
import com.google.common.base.Suppliers;
import org.cryptomator.common.Environment;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.cryptolib.common.P384KeyPair;
import org.cryptomator.cryptolib.common.Pkcs12Exception;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.UUID;
import java.util.function.Supplier;
@Singleton
public class DeviceKey {
private static final Logger LOG = LoggerFactory.getLogger(DeviceKey.class);
private static final String KEYCHAIN_KEY = "cryptomator-device-p12";
private static final String KEYCHAIN_DISPLAY_NAME = "Cryptomator Device Keypair .p12 Passphrase";
private final KeychainManager keychainManager;
private final Environment env;
private final SecureRandom csprng;
private final Supplier<P384KeyPair> keyPairSupplier;
@Inject
public DeviceKey(KeychainManager keychainManager, Environment env, SecureRandom csprng) {
this.keychainManager = keychainManager;
this.env = env;
this.csprng = csprng;
this.keyPairSupplier = Suppliers.memoize(this::loadOrCreate);
}
public P384KeyPair get() throws DeviceKeyRetrievalException {
Preconditions.checkState(keychainManager.isSupported());
return keyPairSupplier.get();
}
private P384KeyPair loadOrCreate() throws DeviceKeyRetrievalException {
Path p12File = env.getP12Path().findFirst().orElseThrow(() -> new DeviceKeyRetrievalException("No path for .p12 file configured"));
char[] passphrase = null;
try {
passphrase = keychainManager.loadPassphrase(KEYCHAIN_KEY);
if (passphrase != null && Files.isRegularFile(p12File)) {
return loadExistingKeyPair(passphrase, p12File);
} else { // (re)generate new key pair if either file or password got lost
passphrase = randomPassword();
keychainManager.storePassphrase(KEYCHAIN_KEY, KEYCHAIN_DISPLAY_NAME, CharBuffer.wrap(passphrase));
return createAndStoreNewKeyPair(passphrase, p12File);
}
} catch (KeychainAccessException e) {
throw new DeviceKeyRetrievalException("Failed to access system keychain", e);
} catch (Pkcs12Exception | IOException e) {
throw new DeviceKeyRetrievalException("Failed to access .p12 file", e);
} finally {
if (passphrase != null) {
Arrays.fill(passphrase, '\0');
}
}
}
private P384KeyPair loadExistingKeyPair(char[] passphrase, Path p12File) throws IOException {
LOG.debug("Loading existing device key from {}", p12File);
return P384KeyPair.load(p12File, passphrase);
}
private P384KeyPair createAndStoreNewKeyPair(char[] passphrase, Path p12File) throws IOException {
var keyPair = P384KeyPair.generate();
LOG.debug("Store new device key to {}", p12File);
keyPair.store(p12File, passphrase);
return keyPair;
}
private char[] randomPassword() {
// this is a fast & easy attempt to create a random string:
var uuid = new UUID(csprng.nextLong(), csprng.nextLong());
return uuid.toString().toCharArray();
}
public static class DeviceKeyRetrievalException extends RuntimeException {
private DeviceKeyRetrievalException(String message) {
super(message);
}
private DeviceKeyRetrievalException(String message, Throwable cause) {
super(message, cause);
}
}
}
@@ -32,11 +32,11 @@ public class Settings {
public static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false;
public static final boolean DEFAULT_CHECK_FOR_UPDATES = false;
public static final boolean DEFAULT_START_HIDDEN = false;
public static final boolean DEFAULT_AUTO_CLOSE_VAULTS = false;
public static final boolean DEFAULT_USE_KEYCHAIN = true;
public static final int DEFAULT_PORT = 42427;
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV;
public static final boolean DEFAULT_DEBUG_MODE = false;
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = VolumeImpl.FUSE;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
@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";
@@ -51,11 +51,11 @@ public class Settings {
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UPDATES);
private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN);
private final BooleanProperty autoCloseVaults = new SimpleBooleanProperty(DEFAULT_AUTO_CLOSE_VAULTS);
private final BooleanProperty useKeychain = new SimpleBooleanProperty(DEFAULT_USE_KEYCHAIN);
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
private final ObjectProperty<WebDavUrlScheme> preferredGvfsScheme = new SimpleObjectProperty<>(DEFAULT_GVFS_SCHEME);
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
private final ObjectProperty<String> keychainProvider = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_PROVIDER);
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
@@ -70,9 +70,6 @@ public class Settings {
private final StringProperty language = new SimpleStringProperty(DEFAULT_LANGUAGE);
private final StringProperty mountService = new SimpleStringProperty();
private Consumer<Settings> saveCmd;
/**
@@ -85,11 +82,11 @@ public class Settings {
askedForUpdateCheck.addListener(this::somethingChanged);
checkForUpdates.addListener(this::somethingChanged);
startHidden.addListener(this::somethingChanged);
autoCloseVaults.addListener(this::somethingChanged);
useKeychain.addListener(this::somethingChanged);
port.addListener(this::somethingChanged);
numTrayNotifications.addListener(this::somethingChanged);
preferredGvfsScheme.addListener(this::somethingChanged);
debugMode.addListener(this::somethingChanged);
preferredVolumeImpl.addListener(this::somethingChanged);
theme.addListener(this::somethingChanged);
keychainProvider.addListener(this::somethingChanged);
userInterfaceOrientation.addListener(this::somethingChanged);
@@ -102,7 +99,6 @@ public class Settings {
windowHeight.addListener(this::somethingChanged);
displayConfiguration.addListener(this::somethingChanged);
language.addListener(this::somethingChanged);
mountService.addListener(this::somethingChanged);
}
void setSaveCmd(Consumer<Settings> saveCmd) {
@@ -137,12 +133,6 @@ public class Settings {
return startHidden;
}
public BooleanProperty autoCloseVaults() {
return autoCloseVaults;
}
public BooleanProperty useKeychain() { return useKeychain; }
public IntegerProperty port() {
return port;
}
@@ -151,12 +141,16 @@ public class Settings {
return numTrayNotifications;
}
public ObjectProperty<WebDavUrlScheme> preferredGvfsScheme() {
return preferredGvfsScheme;
}
public BooleanProperty debugMode() {
return debugMode;
}
public StringProperty mountService() {
return mountService;
public ObjectProperty<VolumeImpl> preferredVolumeImpl() {
return preferredVolumeImpl;
}
public ObjectProperty<UiTheme> theme() {
@@ -204,5 +198,4 @@ public class Settings {
public StringProperty languageProperty() {
return language;
}
}
@@ -41,14 +41,14 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
out.name("askedForUpdateCheck").value(value.askedForUpdateCheck().get());
out.name("checkForUpdatesEnabled").value(value.checkForUpdates().get());
out.name("startHidden").value(value.startHidden().get());
out.name("autoCloseVaults").value(value.autoCloseVaults().get());
out.name("port").value(value.port().get());
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get().name());
out.name("debugMode").value(value.debugMode().get());
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
out.name("theme").value(value.theme().get().name());
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
out.name("keychainProvider").value(value.keychainProvider().get());
out.name("useKeychain").value(value.useKeychain().get());
out.name("licenseKey").value(value.licenseKey().get());
out.name("showMinimizeButton").value(value.showMinimizeButton().get());
out.name("showTrayIcon").value(value.showTrayIcon().get());
@@ -58,7 +58,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
out.name("windowHeight").value((value.windowHeightProperty().get()));
out.name("displayConfiguration").value((value.displayConfigurationProperty().get()));
out.name("language").value((value.languageProperty().get()));
out.name("mountService").value(value.mountService().get());
out.endObject();
}
@@ -82,14 +82,14 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "askedForUpdateCheck" -> settings.askedForUpdateCheck().set(in.nextBoolean());
case "checkForUpdatesEnabled" -> settings.checkForUpdates().set(in.nextBoolean());
case "startHidden" -> settings.startHidden().set(in.nextBoolean());
case "autoCloseVaults" -> settings.autoCloseVaults().set(in.nextBoolean());
case "port" -> settings.port().set(in.nextInt());
case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
case "preferredGvfsScheme" -> settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
case "debugMode" -> settings.debugMode().set(in.nextBoolean());
case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
case "keychainProvider" -> settings.keychainProvider().set(in.nextString());
case "useKeychain" -> settings.useKeychain().set(in.nextBoolean());
case "licenseKey" -> settings.licenseKey().set(in.nextString());
case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean());
case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean());
@@ -99,15 +99,9 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "windowHeight" -> settings.windowHeightProperty().set(in.nextInt());
case "displayConfiguration" -> settings.displayConfigurationProperty().set(in.nextString());
case "language" -> settings.languageProperty().set(in.nextString());
case "mountService" -> {
var token = in.peek();
if (JsonToken.STRING == token) {
settings.mountService().set(in.nextString());
}
}
default -> {
LOG.warn("Unsupported vault setting found in JSON: {}", name);
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
}
}
@@ -117,6 +111,24 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
return settings;
}
private VolumeImpl parsePreferredVolumeImplName(String nioAdapterName) {
try {
return VolumeImpl.valueOf(nioAdapterName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid volume type {}. Defaulting to {}.", nioAdapterName, Settings.DEFAULT_PREFERRED_VOLUME_IMPL);
return Settings.DEFAULT_PREFERRED_VOLUME_IMPL;
}
}
private WebDavUrlScheme parseWebDavUrlSchemePrefix(String webDavUrlSchemeName) {
try {
return WebDavUrlScheme.valueOf(webDavUrlSchemeName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid WebDAV url scheme {}. Defaulting to {}.", webDavUrlSchemeName, Settings.DEFAULT_GVFS_SCHEME);
return Settings.DEFAULT_GVFS_SCHEME;
}
}
private UiTheme parseUiTheme(String uiThemeName) {
try {
return UiTheme.valueOf(uiThemeName.toUpperCase());
@@ -49,12 +49,14 @@ public class SettingsProvider implements Supplier<Settings> {
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
private final Supplier<Settings> settings = Suppliers.memoize(this::load);
private final SettingsJsonAdapter settingsJsonAdapter;
private final Environment env;
private final ScheduledExecutorService scheduler;
private final Gson gson;
@Inject
public SettingsProvider(SettingsJsonAdapter settingsJsonAdapter, Environment env, ScheduledExecutorService scheduler) {
this.settingsJsonAdapter = settingsJsonAdapter;
this.env = env;
this.scheduler = scheduler;
this.gson = new GsonBuilder() //
@@ -116,7 +118,7 @@ public class SettingsProvider implements Supplier<Settings> {
try {
Files.createDirectories(settingsPath.getParent());
Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); //
try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE_NEW); //
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
gson.toJson(settings, writer);
}
@@ -6,11 +6,12 @@
package org.cryptomator.common.settings;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringExpression;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
@@ -21,6 +22,7 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
/**
@@ -30,6 +32,7 @@ public class VaultSettings {
public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false;
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 = "";
public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
@@ -42,32 +45,26 @@ public class VaultSettings {
private final String id;
private final ObjectProperty<Path> 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_REVEAL_AFTER_MOUNT);
private final BooleanProperty useCustomMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
private final StringProperty customMountPath = new SimpleStringProperty();
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE);
private final IntegerProperty autoLockIdleSeconds = new SimpleIntegerProperty(DEFAULT_AUTOLOCK_IDLE_SECONDS);
private final StringExpression mountName;
private final ObjectProperty<Path> mountPoint = new SimpleObjectProperty<>();
private final StringBinding mountName;
public VaultSettings(String id) {
this.id = Objects.requireNonNull(id);
this.mountName = StringExpression.stringExpression(Bindings.createStringBinding(() -> {
final String name;
if (displayName.isEmpty().get()) {
name = path.get().getFileName().toString();
} else {
name = displayName.get();
}
return normalizeDisplayName(name);
}, displayName, path));
this.mountName = Bindings.createStringBinding(this::normalizeDisplayName, displayName);
}
Observable[] observables() {
return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode};
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock, autoLockWhenIdle, autoLockIdleSeconds};
}
public static VaultSettings withRandomId() {
@@ -81,7 +78,8 @@ public class VaultSettings {
}
//visible for testing
static String normalizeDisplayName(String original) {
String normalizeDisplayName() {
var original = displayName.getValueSafe();
if (original.isBlank() || ".".equals(original) || "..".equals(original)) {
return "_";
}
@@ -107,10 +105,22 @@ public class VaultSettings {
return displayName;
}
public StringExpression mountName() {
public StringBinding mountName() {
return mountName;
}
public StringProperty winDriveLetter() {
return winDriveLetter;
}
public Optional<String> getWinDriveLetter() {
String current = this.winDriveLetter.get();
if (!Strings.isNullOrEmpty(current)) {
return Optional.of(current);
}
return Optional.empty();
}
public BooleanProperty unlockAfterStartup() {
return unlockAfterStartup;
}
@@ -119,12 +129,20 @@ public class VaultSettings {
return revealAfterMount;
}
public Path getMountPoint() {
return mountPoint.get();
public BooleanProperty useCustomMountPath() {
return useCustomMountPath;
}
public ObjectProperty<Path> mountPoint() {
return mountPoint;
public StringProperty customMountPath() {
return customMountPath;
}
public Optional<String> getCustomMountPath() {
if (useCustomMountPath.get()) {
return Optional.ofNullable(Strings.emptyToNull(customMountPath.get()));
} else {
return Optional.empty();
}
}
public BooleanProperty usesReadOnlyMode() {
@@ -170,4 +188,5 @@ public class VaultSettings {
return false;
}
}
}
@@ -6,14 +6,11 @@
package org.cryptomator.common.settings;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
class VaultSettingsJsonAdapter {
@@ -25,10 +22,11 @@ class VaultSettingsJsonAdapter {
out.name("id").value(value.getId());
out.name("path").value(value.path().get().toString());
out.name("displayName").value(value.displayName().get());
out.name("winDriveLetter").value(value.winDriveLetter().get());
out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
out.name("revealAfterMount").value(value.revealAfterMount().get());
var mountPoint = value.mountPoint().get();
out.name("mountPoint").value(mountPoint != null ? mountPoint.toAbsolutePath().toString() : null);
out.name("useCustomMountPath").value(value.useCustomMountPath().get());
out.name("customMountPath").value(value.customMountPath().get());
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
out.name("mountFlags").value(value.mountFlags().get());
out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get());
@@ -38,18 +36,18 @@ class VaultSettingsJsonAdapter {
out.endObject();
}
//TODO: usesCustomMountPath, customMountPath and winDriveLetter removed
// -> migration required
public VaultSettings read(JsonReader in) throws IOException {
String id = null;
String path = null;
String mountName = null; //see https://github.com/cryptomator/cryptomator/pull/1318
String displayName = null;
String customMountPath = null;
String winDriveLetter = null;
boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
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;
Path mountPoint = null;
int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
boolean autoLockWhenIdle = VaultSettings.DEFAULT_AUTOLOCK_WHEN_IDLE;
@@ -63,23 +61,19 @@ class VaultSettingsJsonAdapter {
case "path" -> path = in.nextString();
case "mountName" -> mountName = in.nextString(); //see https://github.com/cryptomator/cryptomator/pull/1318
case "displayName" -> displayName = in.nextString();
case "winDriveLetter" -> winDriveLetter = in.nextString();
case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean();
case "revealAfterMount" -> revealAfterMount = in.nextBoolean();
case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
case "mountFlags" -> mountFlags = in.nextString();
case "mountPoint" -> {
if (JsonToken.NULL == in.peek()) {
in.nextNull();
} else {
mountPoint = parseMountPoint(in.nextString());
}
}
case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
case "autoLockWhenIdle" -> autoLockWhenIdle = in.nextBoolean();
case "autoLockIdleSeconds" -> autoLockIdleSeconds = in.nextInt();
default -> {
LOG.warn("Unsupported vault setting found in JSON: {}", name);
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
}
}
@@ -93,27 +87,20 @@ class VaultSettingsJsonAdapter {
vaultSettings.displayName().set(mountName);
}
vaultSettings.path().set(Paths.get(path));
vaultSettings.winDriveLetter().set(winDriveLetter);
vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
vaultSettings.revealAfterMount().set(revealAfterMount);
vaultSettings.useCustomMountPath().set(useCustomMountPath);
vaultSettings.customMountPath().set(customMountPath);
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
vaultSettings.mountFlags().set(mountFlags);
vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
vaultSettings.autoLockWhenIdle().set(autoLockWhenIdle);
vaultSettings.autoLockIdleSeconds().set(autoLockIdleSeconds);
vaultSettings.mountPoint().set(mountPoint);
return vaultSettings;
}
private Path parseMountPoint(String mountPoint) {
try {
return Path.of(mountPoint);
} catch (InvalidPathException e) {
LOG.warn("Invalid string as mount point. Defaulting to null.");
return null;
}
}
private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) {
try {
return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase());
@@ -0,0 +1,22 @@
package org.cryptomator.common.settings;
public enum WebDavUrlScheme {
DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");
private final String prefix;
private final String displayName;
WebDavUrlScheme(String prefix, String displayName) {
this.prefix = prefix;
this.displayName = displayName;
}
public String getPrefix() {
return prefix;
}
public String getDisplayName() {
return displayName;
}
}
@@ -0,0 +1,46 @@
package org.cryptomator.common.vaults;
import com.google.common.collect.Iterables;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.mountpoint.MountPointChooser;
import java.nio.file.Path;
import java.util.Optional;
public abstract class AbstractVolume implements Volume {
private final Iterable<MountPointChooser> choosers;
protected Path mountPoint;
private boolean cleanupRequired;
private MountPointChooser usedChooser;
public AbstractVolume(Iterable<MountPointChooser> choosers) {
this.choosers = choosers;
}
protected Path determineMountPoint() throws InvalidMountPointException {
var applicableChoosers = Iterables.filter(choosers, c -> c.isApplicable(this));
for (var chooser : applicableChoosers) {
Optional<Path> chosenPath = chooser.chooseMountPoint(this);
if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint
continue;
}
this.cleanupRequired = chooser.prepare(this, chosenPath.get());
this.usedChooser = chooser;
return chosenPath.get();
}
throw new InvalidMountPointException(String.format("No feasible MountPoint found by choosers: %s", applicableChoosers));
}
protected void cleanupMountPoint() {
if (this.cleanupRequired) {
this.usedChooser.cleanup(this, this.mountPoint);
}
}
@Override
public Optional<Path> getMountPoint() {
return Optional.ofNullable(mountPoint);
}
}
@@ -1,13 +1,11 @@
package org.cryptomator.common.vaults;
import org.cryptomator.integrations.mount.UnmountFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javafx.collections.ObservableList;
import java.io.IOException;
import java.time.Instant;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -41,13 +39,14 @@ public class AutoLocker {
try {
vault.lock(false);
LOG.info("Autolocked {} after idle timeout", vault.getDisplayName());
} catch (UnmountFailedException | IOException e) {
} catch (Volume.VolumeException | LockNotCompletedException e) {
LOG.error("Autolocking failed.", e);
}
}
private boolean exceedsIdleTime(Vault vault) {
assert vault.isUnlocked();
// TODO: shouldn't we read these properties from within FX Application Thread?
if (vault.getVaultSettings().autoLockWhenIdle().get()) {
int maxIdleSeconds = vault.getVaultSettings().autoLockIdleSeconds().get();
var deadline = vault.getStats().getLastActivity().plusSeconds(maxIdleSeconds);
@@ -0,0 +1,14 @@
package org.cryptomator.common.vaults;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@interface DefaultMountFlags {
}
@@ -0,0 +1,95 @@
package org.cryptomator.common.vaults;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.mountpoint.MountPointChooser;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.dokany.DokanyMountFailedException;
import org.cryptomator.frontend.dokany.Mount;
import org.cryptomator.frontend.dokany.MountFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.function.Consumer;
public class DokanyVolume extends AbstractVolume {
private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class);
private static final String FS_TYPE_NAME = "CryptomatorFS";
private final VaultSettings vaultSettings;
private Mount mount;
@Inject
public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
super(choosers);
this.vaultSettings = vaultSettings;
}
@Override
public VolumeImpl getImplementationType() {
return VolumeImpl.DOKANY;
}
@Override
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
this.mountPoint = determineMountPoint();
try {
this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction);
} catch (DokanyMountFailedException e) {
if (vaultSettings.getCustomMountPath().isPresent()) {
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
}
throw new VolumeException("Unable to mount Filesystem", e);
}
}
@Override
public void reveal(Revealer revealer) throws VolumeException {
try {
mount.reveal(revealer::reveal);
} catch (Exception e) {
throw new VolumeException(e);
}
}
@Override
public void unmount() throws VolumeException {
try {
mount.unmount();
} catch (IllegalStateException e) {
throw new VolumeException("Unmount Failed.", e);
}
cleanupMountPoint();
}
@Override
public void unmountForced() {
mount.unmountForced();
cleanupMountPoint();
}
@Override
public boolean supportsForcedUnmount() {
return true;
}
@Override
public boolean isSupported() {
return DokanyVolume.isSupportedStatic();
}
@Override
public MountPointRequirement getMountPointRequirement() {
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.EMPTY_MOUNT_POINT;
}
public static boolean isSupportedStatic() {
return MountFactory.isApplicable();
}
}
@@ -0,0 +1,136 @@
package org.cryptomator.common.vaults;
import com.google.common.collect.Iterators;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.mountpoint.MountPointChooser;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
import org.cryptomator.frontend.fuse.mount.FuseMountException;
import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
import org.cryptomator.frontend.fuse.mount.Mount;
import org.cryptomator.frontend.fuse.mount.Mounter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;
public class FuseVolume extends AbstractVolume {
private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class);
private static final Pattern NON_WHITESPACE_OR_QUOTED = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); // Thanks to https://stackoverflow.com/a/366532
private final VaultSettings vaultSettings;
private Mount mount;
@Inject
public FuseVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
super(choosers);
this.vaultSettings = vaultSettings;
}
@Override
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
this.mountPoint = determineMountPoint();
mount(fs.getPath("/"), mountFlags, onExitAction);
}
private void mount(Path root, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
try {
Mounter mounter = FuseMountFactory.getMounter();
EnvironmentVariables envVars = EnvironmentVariables.create() //
.withFlags(splitFlags(mountFlags)) //
.withMountPoint(mountPoint) //
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
.build();
this.mount = mounter.mount(root, envVars, onExitAction);
} catch (FuseMountException | FuseNotSupportedException e) {
throw new VolumeException("Unable to mount Filesystem", e);
}
}
private String[] splitFlags(String str) {
List<String> flags = new ArrayList<>();
var matches = Iterators.peekingIterator(NON_WHITESPACE_OR_QUOTED.matcher(str).results().iterator());
while (matches.hasNext()) {
String flag = matches.next().group();
// check if flag is missing its argument:
if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(1) != null) { // next is "double quoted"
// next is "double quoted" and flag is missing its argument
flag += matches.next().group(1);
} else if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(2) != null) {
// next is 'single quoted' and flag is missing its argument
flag += matches.next().group(2);
}
flags.add(flag);
}
return flags.toArray(String[]::new);
}
@Override
public void reveal(Revealer revealer) throws VolumeException {
try {
mount.reveal(revealer::reveal);
} catch (Exception e) {
throw new VolumeException(e);
}
}
@Override
public boolean supportsForcedUnmount() {
return true;
}
@Override
public synchronized void unmountForced() throws VolumeException {
try {
mount.unmountForced();
} catch (FuseMountException e) {
throw new VolumeException(e);
}
cleanupMountPoint();
}
@Override
public synchronized void unmount() throws VolumeException {
try {
mount.unmount();
} catch (FuseMountException e) {
throw new VolumeException(e);
}
cleanupMountPoint();
}
@Override
public boolean isSupported() {
return FuseVolume.isSupportedStatic();
}
@Override
public VolumeImpl getImplementationType() {
return VolumeImpl.FUSE;
}
@Override
public MountPointRequirement getMountPointRequirement() {
if (!SystemUtils.IS_OS_WINDOWS) {
return MountPointRequirement.EMPTY_MOUNT_POINT;
}
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.PARENT_NO_MOUNT_POINT;
}
public static boolean isSupportedStatic() {
return FuseMountFactory.isFuseSupported();
}
}
@@ -0,0 +1,12 @@
package org.cryptomator.common.vaults;
public class LockNotCompletedException extends Exception {
public LockNotCompletedException(String reason) {
super(reason);
}
public LockNotCompletedException(Throwable cause) {
super(cause);
}
}
@@ -0,0 +1,33 @@
package org.cryptomator.common.vaults;
/**
* Enumeration used to indicate the requirements for mounting a vault
* using a specific {@link Volume VolumeProvider}, e.g. {@link FuseVolume}.
*/
public enum MountPointRequirement {
/**
* The Mountpoint needs to be a filesystem root and must not exist.
*/
UNUSED_ROOT_DIR,
/**
* No Mountpoint on the local filesystem required. (e.g. WebDAV)
*/
NONE,
/**
* A parent folder is required, but the actual Mountpoint must not exist.
*/
PARENT_NO_MOUNT_POINT,
/**
* A parent folder is required, but the actual Mountpoint may exist.
*/
PARENT_OPT_MOUNT_POINT,
/**
* The actual Mountpoint must exist and must be empty.
*/
EMPTY_MOUNT_POINT;
}
@@ -11,10 +11,9 @@ package org.cryptomator.common.vaults;
import com.google.common.base.Strings;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Constants;
import org.cryptomator.common.Environment;
import org.cryptomator.common.mount.WindowsDriveLetters;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Volume.VolumeException;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
@@ -23,42 +22,29 @@ import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.integrations.mount.Mount;
import org.cryptomator.integrations.mount.MountBuilder;
import org.cryptomator.integrations.mount.MountCapability;
import org.cryptomator.integrations.mount.MountFailedException;
import org.cryptomator.integrations.mount.MountService;
import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.integrations.mount.UnmountFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER;
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR;
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_SYSTEM_CHOSEN_PATH;
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_WITHIN_EXISTING_PARENT;
@PerVault
public class Vault {
@@ -66,16 +52,15 @@ public class Vault {
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
private final Environment env;
private final Settings settings;
private final VaultSettings vaultSettings;
private final Provider<Volume> volumeProvider;
private final StringBinding defaultMountFlags;
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
private final VaultState state;
private final ObjectProperty<Exception> lastKnownException;
private final ObservableValue<MountService> mountService;
private final ObservableValue<String> defaultMountFlags;
private final VaultConfigCache configCache;
private final VaultStats stats;
private final StringBinding displayName;
private final StringBinding displayablePath;
private final BooleanBinding locked;
private final BooleanBinding processing;
@@ -83,24 +68,23 @@ public class Vault {
private final BooleanBinding missing;
private final BooleanBinding needsMigration;
private final BooleanBinding unknownError;
private final ObjectBinding<Mountpoint> mountPoint;
private final WindowsDriveLetters windowsDriveLetters;
private final StringBinding accessPoint;
private final BooleanBinding accessPointPresent;
private final BooleanProperty showingStats;
private AtomicReference<MountHandle> mountHandle = new AtomicReference<>(null);
private volatile Volume volume;
@Inject
Vault(Environment env, Settings settings, VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, ObservableValue<MountService> mountService, VaultStats stats, WindowsDriveLetters windowsDriveLetters) {
this.env = env;
this.settings = settings;
Vault(VaultSettings vaultSettings, VaultConfigCache configCache, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
this.vaultSettings = vaultSettings;
this.configCache = configCache;
this.volumeProvider = volumeProvider;
this.defaultMountFlags = defaultMountFlags;
this.cryptoFileSystem = cryptoFileSystem;
this.state = state;
this.lastKnownException = lastKnownException;
this.mountService = mountService;
this.defaultMountFlags = mountService.map(MountService::getDefaultMountFlags);
this.stats = stats;
this.displayName = Bindings.createStringBinding(this::getDisplayName, vaultSettings.displayName());
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
this.processing = Bindings.createBooleanBinding(this::isProcessing, state);
@@ -108,8 +92,8 @@ public class Vault {
this.missing = Bindings.createBooleanBinding(this::isMissing, state);
this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state);
this.windowsDriveLetters = windowsDriveLetters;
this.accessPoint = Bindings.createStringBinding(this::getAccessPoint, state);
this.accessPointPresent = this.accessPoint.isNotEmpty();
this.showingStats = new SimpleBooleanProperty(false);
}
@@ -159,46 +143,7 @@ public class Vault {
}
}
private MountBuilder prepareMount(Path cryptoRoot) throws IOException {
var mountProvider = mountService.getValue();
var builder = mountProvider.forFileSystem(cryptoRoot);
for (var capability : mountProvider.capabilities()) {
switch (capability) {
case FILE_SYSTEM_NAME -> builder.setFileSystemName("crypto");
case LOOPBACK_PORT -> builder.setLoopbackPort(settings.port().get()); //TODO: move port from settings to vaultsettings?
case LOOPBACK_HOST_NAME -> builder.setLoopbackHostName("cryptomator-vault"); //TODO: Read from system property
case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode().get());
case MOUNT_FLAGS -> builder.setMountFlags(defaultMountFlags.getValue()); // TODO use custom mount flags (pre-populated with default mount flags)
case VOLUME_ID -> builder.setVolumeId(vaultSettings.getId());
case VOLUME_NAME -> builder.setVolumeName(vaultSettings.mountName().get());
}
}
var userChosenMountPoint = vaultSettings.getMountPoint();
var defaultMountPointBase = env.getMountPointsDir().orElseThrow();
if (userChosenMountPoint == null) {
if (mountProvider.hasCapability(MOUNT_TO_SYSTEM_CHOSEN_PATH)) {
// no need to set a mount point
} else if (mountProvider.hasCapability(MOUNT_AS_DRIVE_LETTER)) {
builder.setMountpoint(windowsDriveLetters.getFirstDesiredAvailable().orElseThrow());
} else if (mountProvider.hasCapability(MOUNT_WITHIN_EXISTING_PARENT)) {
Files.createDirectories(defaultMountPointBase);
builder.setMountpoint(defaultMountPointBase);
} else if (mountProvider.hasCapability(MOUNT_TO_EXISTING_DIR) ) {
var mountPoint = defaultMountPointBase.resolve(vaultSettings.mountName().get());
Files.createDirectories(mountPoint);
builder.setMountpoint(mountPoint);
}
} else if (mountProvider.hasCapability(MOUNT_TO_EXISTING_DIR) || mountProvider.hasCapability(MOUNT_WITHIN_EXISTING_PARENT) || mountProvider.hasCapability(MOUNT_AS_DRIVE_LETTER)) {
// TODO: move the mount point away in case of MOUNT_WITHIN_EXISTING_PARENT?
builder.setMountpoint(userChosenMountPoint);
}
return builder;
}
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, MountFailedException {
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
if (cryptoFileSystem.get() != null) {
throw new IllegalStateException("Already unlocked.");
}
@@ -206,10 +151,9 @@ public class Vault {
boolean success = false;
try {
cryptoFileSystem.set(fs);
var rootPath = fs.getRootDirectories().iterator().next();
var supportsForcedUnmount = mountService.getValue().hasCapability(MountCapability.UNMOUNT_FORCED);
var mountHandle = new MountHandle(prepareMount(rootPath).mount(), supportsForcedUnmount);
success = this.mountHandle.compareAndSet(null, mountHandle);
volume = volumeProvider.get();
volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit);
success = true;
} finally {
if (!success) {
destroyCryptoFileSystem();
@@ -217,28 +161,37 @@ public class Vault {
}
}
public synchronized void lock(boolean forced) throws UnmountFailedException, IOException {
var mountHandle = this.mountHandle.get();
if (mountHandle == null) {
//TODO: noop or InvalidStateException?
return;
private void lockOnVolumeExit(Throwable t) {
LOG.info("Unmounted vault '{}'", getDisplayName());
destroyCryptoFileSystem();
state.set(VaultState.Value.LOCKED);
if (t != null) {
LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
}
}
if (forced && mountHandle.supportsUnmountForced) {
mountHandle.mount.unmountForced();
public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException {
//initiate unmount
if (forced && volume.supportsForcedUnmount()) {
volume.unmountForced();
} else {
mountHandle.mount.unmount();
volume.unmount();
}
//wait for lockOnVolumeExit to be executed
try {
mountHandle.mount.close();
} finally {
destroyCryptoFileSystem();
boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS);
if (!locked) {
throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LockNotCompletedException(e);
}
}
this.mountHandle.set(null);
LOG.info("Locked vault '{}'", getDisplayName());
public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
volume.reveal(vaultRevealer);
}
// ******************************************************************************
@@ -313,21 +266,33 @@ public class Vault {
return state.get() == VaultState.Value.ERROR;
}
public ReadOnlyStringProperty displayNameProperty() {
return vaultSettings.displayName();
public StringBinding displayNameProperty() {
return displayName;
}
public String getDisplayName() {
return vaultSettings.displayName().get();
}
public ObjectBinding<Mountpoint> mountPointProperty() {
return mountPoint;
public StringBinding accessPointProperty() {
return accessPoint;
}
public Mountpoint getMountPoint() {
var handle = mountHandle.get();
return handle == null ? null : handle.mount.getMountpoint();
public String getAccessPoint() {
if (state.getValue() == VaultState.Value.UNLOCKED) {
assert volume != null;
return volume.getMountPoint().orElse(Path.of("")).toString();
} else {
return "";
}
}
public BooleanBinding accessPointPresentProperty() {
return accessPointPresent;
}
public boolean isAccessPointPresent() {
return accessPointPresent.get();
}
public StringBinding displayablePathProperty() {
@@ -350,7 +315,7 @@ public class Vault {
}
public boolean isShowingStats() {
return mountHandle.get() != null;
return accessPointPresent.get();
}
@@ -379,18 +344,18 @@ public class Vault {
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
}
public ObservableValue<String> defaultMountFlagsProperty() {
public StringBinding defaultMountFlagsProperty() {
return defaultMountFlags;
}
public String getDefaultMountFlags() {
return defaultMountFlags.getValue();
return defaultMountFlags.get();
}
public String getEffectiveMountFlags() {
String mountFlags = vaultSettings.mountFlags().get();
if (Strings.isNullOrEmpty(mountFlags)) {
return ""; //TODO: should the provider provide dem defaults??
return getDefaultMountFlags();
} else {
return mountFlags;
}
@@ -408,6 +373,10 @@ public class Vault {
return vaultSettings.getId();
}
public Optional<Volume> getVolume() {
return Optional.ofNullable(this.volume);
}
// ******************************************************************************
// Hashcode / Equals
// *******************************************************************************/
@@ -426,15 +395,7 @@ public class Vault {
}
}
/* TODO: reactivate/ needed at all?
public boolean supportsForcedUnmount() {
return volume.supportsForcedUnmount();
}
*/
private record MountHandle(Mount mount, boolean supportsUnmountForced) {
}
}
@@ -8,12 +8,13 @@ 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.inject.Named;
@PerVault
@Subcomponent(modules = {VaultModule.class})
@Subcomponent(modules = {VaultModule.class, MountPointChooserModule.class})
public interface VaultComponent {
Vault vault();
@@ -7,14 +7,26 @@ 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;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicReference;
@Module
@@ -35,4 +47,130 @@ public class VaultModule {
return new SimpleObjectProperty<>(initialErrorCause);
}
@Provides
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
if (VolumeImpl.DOKANY == preferredImpl && dokanyVolume.isSupported()) {
return dokanyVolume;
} else if (VolumeImpl.FUSE == preferredImpl && fuseVolume.isSupported()) {
return fuseVolume;
} else {
if (VolumeImpl.WEBDAV != preferredImpl) {
LOG.warn("Using WebDAV, because {} is not supported.", preferredImpl.getDisplayName());
}
assert webDavVolume.isSupported();
return webDavVolume;
}
}
@Provides
@PerVault
@DefaultMountFlags
public StringBinding provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
ObjectProperty<VolumeImpl> preferredVolumeImpl = settings.preferredVolumeImpl();
StringBinding mountName = vaultSettings.mountName();
BooleanProperty readOnly = vaultSettings.usesReadOnlyMode();
return Bindings.createStringBinding(() -> {
VolumeImpl v = preferredVolumeImpl.get();
if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_MAC) {
return getMacFuseDefaultMountFlags(mountName, readOnly);
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_LINUX) {
return getLinuxFuseDefaultMountFlags(readOnly);
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_WINDOWS) {
return getWindowsFuseDefaultMountFlags(mountName, readOnly);
} else if (v == VolumeImpl.DOKANY && SystemUtils.IS_OS_WINDOWS) {
return getDokanyDefaultMountFlags(readOnly);
} else {
return "--flags-supported-on-FUSE-or-DOKANY-only";
}
}, mountName, readOnly, preferredVolumeImpl);
}
// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
private String getMacFuseDefaultMountFlags(StringBinding mountName, ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_MAC_OSX;
StringBuilder flags = new StringBuilder();
if (readOnly.get()) {
flags.append(" -ordonly");
}
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
flags.append(" -oatomic_o_trunc");
flags.append(" -oauto_xattr");
flags.append(" -oauto_cache");
flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
try {
Path userHome = Paths.get(System.getProperty("user.home"));
int uid = (int) Files.getAttribute(userHome, "unix:uid");
int gid = (int) Files.getAttribute(userHome, "unix:gid");
flags.append(" -ouid=").append(uid);
flags.append(" -ogid=").append(gid);
} catch (IOException e) {
LOG.error("Could not read uid/gid from USER_HOME", e);
}
return flags.toString().strip();
}
// see https://manpages.debian.org/testing/fuse/mount.fuse.8.en.html
private String getLinuxFuseDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_LINUX;
StringBuilder flags = new StringBuilder();
if (readOnly.get()) {
flags.append(" -oro");
}
flags.append(" -oauto_unmount");
try {
Path userHome = Paths.get(System.getProperty("user.home"));
int uid = (int) Files.getAttribute(userHome, "unix:uid");
int gid = (int) Files.getAttribute(userHome, "unix:gid");
flags.append(" -ouid=").append(uid);
flags.append(" -ogid=").append(gid);
} catch (IOException e) {
LOG.error("Could not read uid/gid from USER_HOME", e);
}
return flags.toString().strip();
}
// see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse_main.c#L53-L62 for syntax guide
// see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse.c#L295-L319 for options (-o <...>)
// see https://github.com/billziss-gh/winfsp/wiki/Frequently-Asked-Questions/5ba00e4be4f5e938eaae6ef1500b331de12dee77 (FUSE 4.) on why the given defaults were chosen
private String getWindowsFuseDefaultMountFlags(StringBinding mountName, ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_WINDOWS;
StringBuilder flags = new StringBuilder();
//WinFSP has no explicit "readonly"-option, nut not setting the group/user-id has the same effect, tho.
//So for the time being not setting them is the way to go...
//See: https://github.com/billziss-gh/winfsp/issues/319
if (!readOnly.get()) {
flags.append(" -ouid=-1");
flags.append(" -ogid=11");
}
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
//Dokany requires this option to be set, WinFSP doesn't seem to share this peculiarity,
//but the option exists. Let's keep this here in case we need it.
// flags.append(" -oThreadCount=").append(5);
return flags.toString().strip();
}
// see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
private String getDokanyDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_WINDOWS;
StringBuilder flags = new StringBuilder();
flags.append(" --options CURRENT_SESSION");
if (readOnly.get()) {
flags.append(",WRITE_PROTECTION");
}
flags.append(" --thread-count 5");
flags.append(" --timeout 10000");
flags.append(" --allocation-unit-size 4096");
flags.append(" --sector-size 4096");
return flags.toString().strip();
}
}
@@ -41,8 +41,6 @@ public class VaultStats {
private final LongProperty totalBytesDecrypted = new SimpleLongProperty();
private final LongProperty filesRead = new SimpleLongProperty();
private final LongProperty filesWritten = new SimpleLongProperty();
private final LongProperty filesAccessed = new SimpleLongProperty();
private final LongProperty totalFilesAccessed = new SimpleLongProperty();
private final ObjectProperty<Instant> lastActivity = new SimpleObjectProperty<>();
@Inject
@@ -84,8 +82,6 @@ public class VaultStats {
var oldAccessCount = filesRead.get() + filesWritten.get();
filesRead.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesRead).orElse(0L));
filesWritten.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesWritten).orElse(0L));
filesAccessed.set(stats.map(CryptoFileSystemStats::pollAmountOfAccesses).orElse(0L));
totalFilesAccessed.set(stats.map(CryptoFileSystemStats::pollTotalAmountOfAccesses).orElse(0L));
var newAccessCount = filesRead.get() + filesWritten.get();
// check for any I/O activity
@@ -192,19 +188,6 @@ public class VaultStats {
public long getFilesWritten() {return filesWritten.get();}
public LongProperty filesAccessed() {
return filesAccessed;}
public long getFilesAccessed() {return filesAccessed.get();}
public LongProperty totalFilesAccessed(){
return totalFilesAccessed;
}
public long getTotalFilesAccessed(){
return totalFilesAccessed.get();
}
public ObjectProperty<Instant> lastActivityProperty() {
return lastActivity;
}
@@ -0,0 +1,102 @@
package org.cryptomator.common.vaults;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.stream.Stream;
/**
* Takes a Volume and uses it to mount an unlocked vault
*/
public interface Volume {
/**
* Checks in constant time whether this volume type is supported on the system running Cryptomator.
*
* @return true if this volume can be mounted
*/
boolean isSupported();
/**
* Gets the corresponding enum type of the {@link VolumeImpl volume implementation ("VolumeImpl")} that is implemented by this Volume.
*
* @return the type of implementation as defined by the {@link VolumeImpl VolumeImpl enum}
*/
VolumeImpl getImplementationType();
/**
* @param fs
* @throws IOException
*/
void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws IOException, VolumeException, InvalidMountPointException;
/**
* Reveals the mounted volume.
* <p>
* The given {@code revealer} might be used to do it, but not necessarily.
*
* @param revealer An object capable of revealing the location of the mounted vault to view the content (e.g. in the default file browser).
* @throws VolumeException
*/
void reveal(Revealer revealer) throws VolumeException;
void unmount() throws VolumeException;
Optional<Path> getMountPoint();
MountPointRequirement getMountPointRequirement();
// optional forced unmounting:
default boolean supportsForcedUnmount() {
return false;
}
default void unmountForced() throws VolumeException {
throw new VolumeException("Operation not supported.");
}
static VolumeImpl[] getCurrentSupportedAdapters() {
return Stream.of(VolumeImpl.values()).filter(impl -> switch (impl) {
case WEBDAV -> WebDavVolume.isSupportedStatic();
case DOKANY -> DokanyVolume.isSupportedStatic();
case FUSE -> FuseVolume.isSupportedStatic();
}).toArray(VolumeImpl[]::new);
}
/**
* Exception thrown when a volume-specific command such as mount/unmount/reveal failed.
*/
class VolumeException extends Exception {
public VolumeException(String message) {
super(message);
}
public VolumeException(Throwable cause) {
super(cause);
}
public VolumeException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Hides and unifies the different Revealer implementations in the different nio-adapters.
*/
@FunctionalInterface
interface Revealer {
void reveal(Path p) throws VolumeException;
}
}
@@ -0,0 +1,170 @@
package org.cryptomator.common.vaults;
import com.google.common.base.CharMatcher;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.frontend.webdav.mount.MountParams;
import org.cryptomator.frontend.webdav.mount.Mounter;
import org.cryptomator.frontend.webdav.servlet.WebDavServletController;
import javax.inject.Inject;
import javax.inject.Provider;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class WebDavVolume implements Volume {
private static final String LOCALHOST_ALIAS = "cryptomator-vault";
private final Provider<WebDavServer> serverProvider;
private final VaultSettings vaultSettings;
private final Settings settings;
private final WindowsDriveLetters windowsDriveLetters;
private WebDavServer server;
private WebDavServletController servlet;
private Mounter.Mount mount;
private Consumer<Throwable> onExitAction;
@Inject
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
this.serverProvider = serverProvider;
this.vaultSettings = vaultSettings;
this.settings = settings;
this.windowsDriveLetters = windowsDriveLetters;
}
@Override
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
startServlet(fs);
mountServlet();
this.onExitAction = onExitAction;
}
private void startServlet(CryptoFileSystem fs) {
if (server == null) {
server = serverProvider.get();
}
if (!server.isRunning()) {
server.start();
}
CharMatcher acceptable = CharMatcher.inRange('0', '9').or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.inRange('a', 'z'));
String urlConformMountName = acceptable.negate().collapseFrom(vaultSettings.mountName().get(), '_');
servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + urlConformMountName);
servlet.start();
}
private void mountServlet() throws VolumeException {
if (servlet == null) {
throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
}
//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specific one or there is no free.
Supplier<String> driveLetterSupplier;
if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
driveLetterSupplier = () -> windowsDriveLetters.getDesiredAvailableDriveLetter().orElse(null);
} else {
driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
}
MountParams mountParams = MountParams.create() //
.withWindowsDriveLetter(driveLetterSupplier.get()) //
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
.withWebdavHostname(getLocalhostAliasOrNull()) //
.build();
try {
this.mount = servlet.mount(mountParams); // might block this thread for a while
} catch (Mounter.CommandFailedException e) {
throw new VolumeException(e);
}
}
@Override
public void reveal(Revealer revealer) throws VolumeException {
try {
mount.reveal(revealer::reveal);
} catch (Exception e) {
throw new VolumeException(e);
}
}
@Override
public synchronized void unmount() throws VolumeException {
try {
mount.unmount();
} catch (Mounter.CommandFailedException e) {
throw new VolumeException(e);
}
cleanup();
onExitAction.accept(null);
}
@Override
public synchronized void unmountForced() throws VolumeException {
try {
mount.forced().orElseThrow(IllegalStateException::new).unmount();
} catch (Mounter.CommandFailedException e) {
throw new VolumeException(e);
}
cleanup();
onExitAction.accept(null);
}
@Override
public Optional<Path> getMountPoint() {
return mount.getMountPoint();
}
@Override
public MountPointRequirement getMountPointRequirement() {
return MountPointRequirement.NONE;
}
private String getLocalhostAliasOrNull() {
try {
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);
if (alias.getHostAddress().equals("127.0.0.1")) {
return LOCALHOST_ALIAS;
} else {
return null;
}
} catch (UnknownHostException e) {
return null;
}
}
private void cleanup() {
if (servlet != null) {
servlet.stop();
}
}
@Override
public boolean isSupported() {
return WebDavVolume.isSupportedStatic();
}
@Override
public VolumeImpl getImplementationType() {
return VolumeImpl.WEBDAV;
}
@Override
public boolean supportsForcedUnmount() {
return mount != null && mount.forced().isPresent();
}
public static boolean isSupportedStatic() {
return true;
}
}
@@ -0,0 +1,85 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common.vaults;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.SystemUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;
@Singleton
public final class WindowsDriveLetters {
private static final Set<String> A_TO_Z;
static {
try (IntStream stream = IntStream.rangeClosed('A', 'Z')) {
A_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(ImmutableSet.toImmutableSet());
}
}
@Inject
public WindowsDriveLetters() {
}
public Set<String> getAllDriveLetters() {
return A_TO_Z;
}
public Set<String> getOccupiedDriveLetters() {
if (!SystemUtils.IS_OS_WINDOWS) {
return Set.of();
} else {
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
return StreamSupport.stream(rootDirs.spliterator(), false).map(p -> p.toString().substring(0, 1)).collect(Collectors.toSet());
}
}
public Set<String> getAvailableDriveLetters() {
return Sets.difference(getAllDriveLetters(), getOccupiedDriveLetters());
}
public Optional<String> getAvailableDriveLetter() {
return getAvailableDriveLetters().stream().findFirst();
}
public Optional<Path> getAvailableDriveLetterPath() {
return getAvailableDriveLetter().map(this::toPath);
}
/**
* Skips A and B and only returns them if all other are occupied.
*
* @return an Optional containing either the letter of a free drive letter or empty, if none is available
*/
public Optional<String> getDesiredAvailableDriveLetter() {
var availableDriveLetters = getAvailableDriveLetters();
var optString = availableDriveLetters.stream().filter(s -> !(s.equals("A") || s.equals("B"))).findFirst();
return optString.or(() -> availableDriveLetters.stream().findFirst());
}
/**
* Skips A and B and only returns them if all other are occupied.
*
* @return an Optional containing either the path to a free drive letter or empty, if none is available
*/
public Optional<Path> getDesiredAvailableDriveLetterPath() {
return getDesiredAvailableDriveLetter().map(this::toPath);
}
public Path toPath(String driveLetter) {
return Path.of(driveLetter + ":\\");
}
}
@@ -12,6 +12,7 @@ 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.fxapp.FxApplicationComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,6 +35,7 @@ public class Cryptomator {
private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.factory().create(STARTUP_TIME);
private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
private final LoggerConfiguration logConfig;
private final DebugMode debugMode;
private final SupportedLanguages supportedLanguages;
private final Environment env;
@@ -41,7 +43,8 @@ public class Cryptomator {
private final ShutdownHook shutdownHook;
@Inject
Cryptomator(DebugMode debugMode, SupportedLanguages supportedLanguages, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, ShutdownHook shutdownHook) {
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, SupportedLanguages supportedLanguages, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, ShutdownHook shutdownHook) {
this.logConfig = logConfig;
this.debugMode = debugMode;
this.supportedLanguages = supportedLanguages;
this.env = env;
@@ -76,9 +79,9 @@ public class Cryptomator {
* @return Nonzero exit code in case of an error.
*/
private int run(String[] args) {
env.log();
logConfig.init();
LOG.debug("Dagger graph initialized after {}ms", System.currentTimeMillis() - STARTUP_TIME);
LOG.info("Starting Cryptomator {} on {} {} ({})", env.getAppVersion(), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
LOG.info("Starting Cryptomator {} on {} {} ({})", env.getAppVersion().orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
debugMode.initialize();
supportedLanguages.applyPreferred();
@@ -3,13 +3,14 @@ package org.cryptomator.launcher;
import dagger.BindsInstance;
import dagger.Component;
import org.cryptomator.common.CommonsModule;
import org.cryptomator.logging.LoggerModule;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import javax.inject.Named;
import javax.inject.Singleton;
@Singleton
@Component(modules = {CryptomatorModule.class, CommonsModule.class})
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class})
public interface CryptomatorComponent {
Cryptomator application();
@@ -15,9 +15,9 @@ public class SupportedLanguages {
private static final Logger LOG = LoggerFactory.getLogger(SupportedLanguages.class);
// these are BCP 47 language codes, not ISO. Note the "-" instead of the "_":
public static final List<String> LANGUAGAE_TAGS = List.of("en", "ar", "be", "bn", "bs", "ca", "cs", "da", "de", "el", "es", "fil", "fa", "fr", "gl", "he", //
"hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "mk", "nb", "nl", "nn", "no", "pa", "pl", "pt", "pt-BR", "ro", "ru", "si", "sk", "sr", "sr-Latn", "sv", "sw", //
"ta", "te", "th", "tr", "uk", "vi", "zh", "zh-HK", "zh-TW");
public static final List<String> LANGUAGAE_TAGS = List.of("en", "ar", "bn", "bs", "ca", "cs", "de", "el", "es", "fil", "fr", "gl", "he", //
"hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "mk", "nb", "nl", "nn", "no", "pa", "pl", "pt", "pt-BR", "ro", "ru", "sk", "sr", //
"sr-Latn", "sv", "sw", "ta", "te", "th", "tr", "uk", "zh", "zh-HK", "zh-TW");
@Nullable
private final String preferredLanguage;
@@ -5,24 +5,29 @@
*******************************************************************************/
package org.cryptomator.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import org.cryptomator.common.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javafx.beans.value.ObservableValue;
import java.util.Map;
@Singleton
public class DebugMode {
private static final Logger LOG = LoggerFactory.getLogger(DebugMode.class);
private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(DebugMode.class);
private final Settings settings;
private final LoggerContext context;
@Inject
public DebugMode(Settings settings) {
public DebugMode(Settings settings, LoggerContext context) {
this.settings = settings;
this.context = context;
}
public void initialize() {
@@ -35,13 +40,19 @@ public class DebugMode {
}
private void setLogLevels(boolean debugMode) {
var configurator = LogbackConfiguratorFactory.provider();
if (debugMode) {
configurator.setLogLevels(LogbackConfigurator.DEBUG_LOG_LEVELS);
setLogLevels(LoggerModule.DEBUG_LOG_LEVELS);
LOG.debug("Debug mode enabled");
} else {
LOG.debug("Debug mode disabled");
configurator.setLogLevels(LogbackConfigurator.DEFAULT_LOG_LEVELS);
setLogLevels(LoggerModule.DEFAULT_LOG_LEVELS);
}
}
private void setLogLevels(Map<String, Level> logLevels) {
for (Map.Entry<String, Level> loglevel : logLevels.entrySet()) {
Logger logger = context.getLogger(loglevel.getKey());
logger.setLevel(loglevel.getValue());
}
}
@@ -1,145 +0,0 @@
package org.cryptomator.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.Configurator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.helpers.NOPAppender;
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.util.FileSize;
import org.cryptomator.common.Environment;
import java.nio.file.Path;
import java.util.Map;
public class LogbackConfigurator extends ContextAwareBase implements Configurator {
private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";
private static final String UPGRADE_FILENAME = "upgrade.log";
private static final String LOGFILE_NAME = "cryptomator0.log";
private static final String LOGFILE_ROLLING_PATTERN = "cryptomator%i.log";
private static final int LOGFILE_ROLLING_MIN = 1;
private static final int LOGFILE_ROLLING_MAX = 9;
private static final String LOG_MAX_SIZE = "100mb";
static final Map<String, Level> DEFAULT_LOG_LEVELS = Map.of( //
Logger.ROOT_LOGGER_NAME, Level.INFO, //
"org.cryptomator", Level.INFO //
);
static final Map<String, Level> DEBUG_LOG_LEVELS = Map.of( //
Logger.ROOT_LOGGER_NAME, Level.INFO, //
"org.cryptomator", Level.TRACE //
);
LogbackConfigurator() {}
/**
* Adjust the log levels
*
* @param logLevels new log levels to use
*/
void setLogLevels(Map<String, Level> logLevels) {
if (context instanceof LoggerContext lc) {
for (var loglevel : logLevels.entrySet()) {
Logger logger = lc.getLogger(loglevel.getKey());
logger.setLevel(loglevel.getValue());
}
}
}
@Override
public ExecutionStatus configure(LoggerContext context) {
var useCustomCfg = Environment.getInstance().useCustomLogbackConfig();
var logDir = Environment.getInstance().getLogDir().orElse(null);
if (useCustomCfg) {
addInfo("Using external logback configuration file.");
} else {
// configure appenders:
var stdout = stdOutAppender(context);
var noop = noopAppender(context);
var file = logDir == null ? noop : fileAppender(context, logDir);
var upgrade = logDir == null ? noop : upgradeAppender(context, logDir);
// configure loggers:
for (var loglevel : DEFAULT_LOG_LEVELS.entrySet()) {
Logger logger = context.getLogger(loglevel.getKey());
logger.setLevel(loglevel.getValue());
logger.setAdditive(false);
logger.addAppender(stdout);
logger.addAppender(file);
}
// configure upgrade logger:
Logger upgrades = context.getLogger("org.cryptomator.cryptofs.migration");
upgrades.setLevel(Level.DEBUG);
upgrades.addAppender(stdout);
upgrades.addAppender(upgrade);
upgrades.addAppender(file);
upgrades.setAdditive(false);
}
return ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY;
}
private Appender<ILoggingEvent> noopAppender(LoggerContext context) {
var appender = new NOPAppender<ILoggingEvent>();
appender.setContext(context);
return appender;
}
private Appender<ILoggingEvent> stdOutAppender(LoggerContext context) {
var appender = new ConsoleAppender<ILoggingEvent>();
appender.setContext(context);
appender.setName("STDOUT");
appender.setEncoder(encoder(context));
appender.start();
return appender;
}
private Appender<ILoggingEvent> upgradeAppender(LoggerContext context, Path logDir) {
var appender = new FileAppender<ILoggingEvent>();
appender.setContext(context);
appender.setName("UPGRADE");
appender.setFile(logDir.resolve(UPGRADE_FILENAME).toString());
appender.setEncoder(encoder(context));
appender.start();
return appender;
}
private Appender<ILoggingEvent> fileAppender(LoggerContext context, Path logDir) {
var appender = new RollingFileAppender<ILoggingEvent>();
appender.setContext(context);
appender.setName("FILE");
appender.setFile(logDir.resolve(LOGFILE_NAME).toString());
appender.setEncoder(encoder(context));
var triggeringPolicy = new LaunchAndSizeBasedTriggeringPolicy<ILoggingEvent>(FileSize.valueOf(LOG_MAX_SIZE));
triggeringPolicy.setContext(context);
triggeringPolicy.start();
appender.setTriggeringPolicy(triggeringPolicy);
var rollingPolicy = new FixedWindowRollingPolicy();
rollingPolicy.setContext(context);
rollingPolicy.setFileNamePattern(logDir.resolve(LOGFILE_ROLLING_PATTERN).toString());
rollingPolicy.setMinIndex(LOGFILE_ROLLING_MIN);
rollingPolicy.setMaxIndex(LOGFILE_ROLLING_MAX);
rollingPolicy.setParent(appender);
rollingPolicy.start();
appender.setRollingPolicy(rollingPolicy);
appender.start();
return appender;
}
private PatternLayoutEncoder encoder(LoggerContext context) {
var encoder = new PatternLayoutEncoder();
encoder.setContext(context);
encoder.setPattern(LOG_PATTERN);
encoder.start();
return encoder;
}
}
@@ -1,12 +0,0 @@
package org.cryptomator.logging;
public class LogbackConfiguratorFactory {
public static LogbackConfigurator provider() {
final class Holder {
private static final LogbackConfigurator INSTANCE = new LogbackConfigurator();
}
return Holder.INSTANCE;
}
}
@@ -0,0 +1,70 @@
package org.cryptomator.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import org.cryptomator.common.Environment;
import org.cryptomator.common.ShutdownHook;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Map;
@Singleton
public class LoggerConfiguration {
private final LoggerContext context;
private final Environment environment;
private final Appender<ILoggingEvent> stdout;
private final Appender<ILoggingEvent> upgrade;
private final Appender<ILoggingEvent> file;
private final ShutdownHook shutdownHook;
@Inject
LoggerConfiguration(LoggerContext context, //
Environment environment, //
@Named("stdoutAppender") Appender<ILoggingEvent> stdout, //
@Named("upgradeAppender") Appender<ILoggingEvent> upgrade, //
@Named("fileAppender") Appender<ILoggingEvent> file, //
ShutdownHook shutdownHook) {
this.context = context;
this.environment = environment;
this.stdout = stdout;
this.upgrade = upgrade;
this.file = file;
this.shutdownHook = shutdownHook;
}
public void init() {
if (environment.useCustomLogbackConfig()) {
Logger root = context.getLogger(Logger.ROOT_LOGGER_NAME);
root.info("Using external logback configuration file.");
} else {
context.reset();
// configure loggers:
for (Map.Entry<String, Level> loglevel : LoggerModule.DEFAULT_LOG_LEVELS.entrySet()) {
Logger logger = context.getLogger(loglevel.getKey());
logger.setLevel(loglevel.getValue());
logger.setAdditive(false);
logger.addAppender(stdout);
logger.addAppender(file);
}
// configure upgrade logger:
Logger upgrades = context.getLogger("org.cryptomator.cryptofs.migration");
upgrades.setLevel(Level.DEBUG);
upgrades.addAppender(stdout);
upgrades.addAppender(upgrade);
upgrades.addAppender(file);
upgrades.setAdditive(false);
// add shutdown hook
shutdownHook.runOnShutdown(ShutdownHook.PRIO_LAST, context::stop);
}
}
}
@@ -0,0 +1,130 @@
package org.cryptomator.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.helpers.NOPAppender;
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.util.FileSize;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.Environment;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.util.Map;
@Module
public class LoggerModule {
private static final String UPGRADE_FILENAME = "upgrade.log";
private static final String LOGFILE_NAME = "cryptomator0.log";
private static final String LOGFILE_ROLLING_PATTERN = "cryptomator%i.log";
private static final int LOGFILE_ROLLING_MIN = 1;
private static final int LOGFILE_ROLLING_MAX = 9;
private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";
private static final String LOG_MAX_SIZE = "100mb";
static final Map<String, Level> DEFAULT_LOG_LEVELS = Map.of( //
Logger.ROOT_LOGGER_NAME, Level.INFO, //
"org.cryptomator", Level.INFO //
);
static final Map<String, Level> DEBUG_LOG_LEVELS = Map.of( //
Logger.ROOT_LOGGER_NAME, Level.INFO, //
"org.cryptomator", Level.TRACE //
);
@Provides
@Singleton
static LoggerContext provideLoggerContext() {
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
if (loggerFactory instanceof LoggerContext context) {
return context;
} else {
throw new IllegalStateException("SLF4J not bound to Logback.");
}
}
@Provides
@Singleton
static PatternLayoutEncoder provideLayoutEncoder(LoggerContext context) {
PatternLayoutEncoder ple = new PatternLayoutEncoder();
ple.setPattern(LOG_PATTERN);
ple.setContext(context);
ple.start();
return ple;
}
@Provides
@Singleton
@Named("stdoutAppender")
static Appender<ILoggingEvent> provideStdoutAppender(LoggerContext context, PatternLayoutEncoder encoder) {
ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
appender.setContext(context);
appender.setEncoder(encoder);
appender.start();
return appender;
}
@Provides
@Singleton
@Named("fileAppender")
static Appender<ILoggingEvent> provideFileAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) {
var optionalLogDir = environment.getLogDir();
if (optionalLogDir.isPresent()) {
Path logDir = optionalLogDir.get();
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
appender.setContext(context);
appender.setFile(logDir.resolve(LOGFILE_NAME).toString());
appender.setEncoder(encoder);
LaunchAndSizeBasedTriggeringPolicy triggeringPolicy = new LaunchAndSizeBasedTriggeringPolicy(FileSize.valueOf(LOG_MAX_SIZE));
triggeringPolicy.setContext(context);
triggeringPolicy.start();
appender.setTriggeringPolicy(triggeringPolicy);
FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
rollingPolicy.setContext(context);
rollingPolicy.setFileNamePattern(logDir.resolve(LOGFILE_ROLLING_PATTERN).toString());
rollingPolicy.setMinIndex(LOGFILE_ROLLING_MIN);
rollingPolicy.setMaxIndex(LOGFILE_ROLLING_MAX);
rollingPolicy.setParent(appender);
rollingPolicy.start();
appender.setRollingPolicy(rollingPolicy);
appender.start();
return appender;
} else {
NOPAppender appender = new NOPAppender<>();
appender.setContext(context);
return appender;
}
}
@Provides
@Singleton
@Named("upgradeAppender")
static Appender<ILoggingEvent> provideUpgradeAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) {
var optionalLogDir = environment.getLogDir();
if (optionalLogDir.isPresent()) {
FileAppender<ILoggingEvent> appender = new FileAppender<>();
appender.setFile(optionalLogDir.get().resolve(UPGRADE_FILENAME).toString());
appender.setContext(context);
appender.setEncoder(encoder);
appender.start();
return appender;
} else {
NOPAppender appender = new NOPAppender<>();
appender.setContext(context);
return appender;
}
}
}
@@ -2,20 +2,19 @@ package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.integrations.uiappearance.Theme;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.fxapp.FxApplicationStyle;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.image.Image;
@@ -24,7 +23,6 @@ import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects;
import java.util.ResourceBundle;
import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_GLOB;
@@ -42,10 +40,12 @@ public class ChooseExistingVaultController implements FxController {
private final ObjectProperty<Vault> vault;
private final VaultListManager vaultListManager;
private final ResourceBundle resourceBundle;
private final ObservableValue<Image> screenshot;
private final Settings settings;
private Image screenshot;
@Inject
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, FxApplicationWindows appWindows, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle, FxApplicationStyle applicationStyle) {
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, FxApplicationWindows appWindows, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle, Settings settings) {
this.window = window;
this.welcomeScene = welcomeScene;
this.successScene = successScene;
@@ -54,20 +54,16 @@ public class ChooseExistingVaultController implements FxController {
this.vault = vault;
this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle;
this.screenshot = applicationStyle.appliedThemeProperty().map(this::selectScreenshot);
this.settings = settings;
}
private Image selectScreenshot(Theme theme) {
String imageResourcePath;
@FXML
public void initialize() {
if (SystemUtils.IS_OS_MAC) {
imageResourcePath = switch (theme) {
case LIGHT -> "/img/select-masterkey-mac.png";
case DARK -> "/img/select-masterkey-mac-dark.png";
};
this.screenshot = new Image(getClass().getResource("/img/select-masterkey-mac"+(UiTheme.LIGHT == settings.theme().get()? "":"-dark")+".png").toString());
} else {
imageResourcePath = "/img/select-masterkey-win.png";
this.screenshot = new Image(getClass().getResource("/img/select-masterkey-win.png").toString());
}
return new Image((Objects.requireNonNull(getClass().getResource(imageResourcePath)).toString()));
}
@FXML
@@ -96,13 +92,8 @@ public class ChooseExistingVaultController implements FxController {
/* Getter */
public ObservableValue<Image> screenshotProperty() {
public Image getScreenshot() {
return screenshot;
}
public Image getScreenshot() {
return screenshot.getValue();
}
}
@@ -10,6 +10,7 @@ import javax.inject.Named;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
@@ -32,6 +33,8 @@ public class CreateNewVaultNameController implements FxController {
private final ObjectProperty<Path> vaultPath;
private final StringProperty vaultName;
private final BooleanBinding validVaultName;
private final BooleanBinding invalidVaultName;
private final StringBinding warningText;
@Inject
CreateNewVaultNameController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
@@ -41,14 +44,20 @@ public class CreateNewVaultNameController implements FxController {
this.vaultPath = vaultPath;
this.vaultName = vaultName;
this.validVaultName = Bindings.createBooleanBinding(this::isValidVaultName, vaultName);
this.invalidVaultName = validVaultName.not();
this.warningText = Bindings.when(vaultName.isNotEmpty().and(invalidVaultName)).then(resourceBundle.getString("addvaultwizard.new.invalidName")).otherwise((String) null);
}
@FXML
public void initialize() {
vaultName.bindBidirectional(textField.textProperty());
vaultName.bind(textField.textProperty());
vaultName.addListener(this::vaultNameChanged);
}
public boolean isValidVaultName() {
return vaultName.get() != null && VALID_NAME_PATTERN.matcher(vaultName.get().trim()).matches();
}
private void vaultNameChanged(@SuppressWarnings("unused") Observable observable) {
if (isValidVaultName()) {
if (vaultPath.get() != null) {
@@ -66,17 +75,32 @@ public class CreateNewVaultNameController implements FxController {
@FXML
public void next() {
window.setScene(chooseLocationScene.get());
vaultName.set(vaultName.get().trim());
}
/* Getter/Setter */
public BooleanBinding validVaultNameProperty() {
return validVaultName;
public BooleanBinding invalidVaultNameProperty() {
return invalidVaultName;
}
public boolean isValidVaultName() {
return vaultName.get() != null && VALID_NAME_PATTERN.matcher(vaultName.get().trim()).matches();
public boolean isInvalidVaultName() {
return invalidVaultName.get();
}
public StringBinding warningTextProperty() {
return warningText;
}
public String getWarningText() {
return warningText.get();
}
public BooleanBinding showWarningProperty() {
return warningText.isNotEmpty();
}
public boolean isShowWarning() {
return showWarningProperty().get();
}
}
@@ -102,7 +102,7 @@ public class CreateNewVaultPasswordController implements FxController {
this.masterkeyFileAccess = masterkeyFileAccess;
this.processing = new SimpleBooleanProperty();
this.readyToCreateVault = new SimpleBooleanProperty();
this.createVaultButtonState = Bindings.when(processing).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
}
@FXML
@@ -231,6 +231,6 @@ public class CreateNewVaultPasswordController implements FxController {
}
public ContentDisplay getCreateVaultButtonState() {
return createVaultButtonState.get();
return processing.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
}
}
@@ -12,15 +12,15 @@ import javafx.beans.value.ObservableValue;
* <p>
* During creation the consumer can optionally define actions to be executed everytime before the animation starts and after it stops.
*/
public class AutoAnimator {
public class AutoAnimator<T extends Animation> {
private final Animation animation;
private final T animation;
private final ObservableValue<Boolean> condition;
private final Runnable beforeStart;
private final Runnable afterStop;
private final Subscription sub;
AutoAnimator(Animation animation, ObservableValue<Boolean> condition, Runnable beforeStart, Runnable afterStop) {
AutoAnimator(T animation, ObservableValue<Boolean> condition, Runnable beforeStart, Runnable afterStop) {
this.animation = animation;
this.condition = condition;
this.beforeStart = beforeStart;
@@ -52,7 +52,7 @@ public class AutoAnimator {
public static class Builder {
private final Animation animation;
private Animation animation;
private ObservableValue<Boolean> condition = new SimpleBooleanProperty(true);
private Runnable beforeStart = () -> {};
private Runnable afterStop = () -> {};
@@ -77,7 +77,7 @@ public class ErrorController implements FxController {
var enhancedTemplate = String.format(REPORT_BODY_TEMPLATE, //
System.getProperty("os.name"), //
System.getProperty("os.version"), //
environment.getAppVersion(), //
environment.getAppVersion().orElse("undefined"), //
environment.getBuildNumber().orElse("undefined"));
var body = URLEncoder.encode(enhancedTemplate, StandardCharsets.UTF_8);
application.getHostServices().showDocument(REPORT_URL_FORMAT.formatted(title, body));
@@ -13,13 +13,6 @@ public enum FxmlFile {
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
HEALTH_START("/fxml/health_start.fxml"), //
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), //
HUB_LICENSE_EXCEEDED("/fxml/hub_license_exceeded.fxml"), //
HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), //
HUB_REGISTER_SUCCESS("/fxml/hub_register_success.fxml"), //
HUB_REGISTER_FAILED("/fxml/hub_register_failed.fxml"),
HUB_UNAUTHORIZED_DEVICE("/fxml/hub_unauthorized_device.fxml"), //
LOCK_FORCED("/fxml/lock_forced.fxml"), //
LOCK_FAILED("/fxml/lock_failed.fxml"), //
MAIN_WINDOW("/fxml/main_window.fxml"), //
@@ -30,11 +23,9 @@ public enum FxmlFile {
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
PREFERENCES("/fxml/preferences.fxml"), //
QUIT("/fxml/quit.fxml"), //
QUIT_FORCED("/fxml/quit_forced.fxml"), //
RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), //
RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), //
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
RECOVERYKEY_RESET_PASSWORD_SUCCESS("/fxml/recoverykey_reset_password_success.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
@@ -0,0 +1,25 @@
package org.cryptomator.ui.common;
import dagger.Lazy;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import javax.inject.Inject;
import javafx.application.Application;
import java.nio.file.Path;
@FxApplicationScoped
public class HostServiceRevealer implements Volume.Revealer {
private final Lazy<Application> application;
@Inject
public HostServiceRevealer(Lazy<Application> application) {
this.application = application;
}
@Override
public void reveal(Path p) throws Volume.VolumeException {
application.get().getHostServices().showDocument(p.toUri().toString());
}
}
@@ -1,5 +1,6 @@
package org.cryptomator.ui.common;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
@@ -41,7 +42,7 @@ public class NewPasswordController implements FxController {
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty()));
passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength));
passwordStrengthLabel.textProperty().bind(passwordStrength.map(strengthRater::getStrengthDescription));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::passwordFieldsMatch, passwordField.textProperty(), reenterField.textProperty());
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
@@ -1,20 +1,16 @@
package org.cryptomator.ui.common;
import dagger.Lazy;
import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.integrations.mount.UnmountFailedException;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.concurrent.Task;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
@@ -28,13 +24,13 @@ public class VaultService {
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
private final Lazy<Application> application;
private final ExecutorService executorService;
private final HostServiceRevealer vaultRevealer;
@Inject
public VaultService(Lazy<Application> application, ExecutorService executorService) {
this.application = application;
public VaultService(ExecutorService executorService, HostServiceRevealer vaultRevealer) {
this.executorService = executorService;
this.vaultRevealer = vaultRevealer;
}
public void reveal(Vault vault) {
@@ -47,7 +43,7 @@ public class VaultService {
* @param vault The vault to reveal
*/
public Task<Vault> createRevealTask(Vault vault) {
Task<Vault> task = new RevealVaultTask(vault, application.get().getHostServices());
Task<Vault> task = new RevealVaultTask(vault, vaultRevealer);
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName()));
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException()));
return task;
@@ -90,8 +86,7 @@ public class VaultService {
}
/**
* Creates a lock-all task.
* This task itself is _not started_, but its subtasks locking each vault will be already executed.
* Creates but doesn't start a lock-all task.
*
* @param vaults The list of vaults to be locked
* @param forced Whether to attempt a forced lock
@@ -110,21 +105,22 @@ public class VaultService {
private static class RevealVaultTask extends Task<Vault> {
private final Vault vault;
private final HostServices hostServices;
private final Volume.Revealer revealer;
public RevealVaultTask(Vault vault, HostServices hostServices) {
/**
* @param vault The vault to lock
* @param revealer The object to use to show the vault content to the user.
*/
public RevealVaultTask(Vault vault, Volume.Revealer revealer) {
this.vault = vault;
this.hostServices = hostServices;
this.revealer = revealer;
setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException()));
}
@Override
protected Vault call() {
switch (vault.getMountPoint()) {
case null -> LOG.warn("Not currently mounted");
case Mountpoint.WithPath m -> hostServices.showDocument(m.uri().toString());
case Mountpoint.WithUri m -> LOG.info("Vault mounted at {}", m.uri()); // TODO show in UI?
}
protected Vault call() throws Volume.VolumeException {
vault.reveal(revealer);
return vault;
}
}
@@ -183,7 +179,7 @@ public class VaultService {
}
@Override
protected Vault call() throws UnmountFailedException, IOException {
protected Vault call() throws Volume.VolumeException, LockNotCompletedException {
vault.lock(forced);
return vault;
}
@@ -27,7 +27,6 @@ public enum FontAwesome5Icon {
FILE("\uF15B"), //
FILE_IMPORT("\uF56F"), //
FOLDER_OPEN("\uF07C"), //
FUNNEL("\uF0B0"), //
HAND_HOLDING_HEART("\uF4BE"), //
HEART("\uF004"), //
HDD("\uF0A0"), //
@@ -46,7 +46,7 @@ public class NiceSecurePasswordField extends StackPane {
nonPrintableCharsIcon.managedProperty().bind(passwordField.containingNonPrintableCharsProperty());
revealPasswordIcon.setGlyph(FontAwesome5Icon.EYE);
revealPasswordIcon.glyphProperty().bind(Bindings.when(revealPasswordButton.selectedProperty()).then(FontAwesome5Icon.EYE_SLASH).otherwise(FontAwesome5Icon.EYE));
revealPasswordIcon.glyphProperty().bind(Bindings.createObjectBinding(this::getRevealPasswordGlyph, revealPasswordButton.selectedProperty()));
revealPasswordIcon.setGlyphSize(ICON_SIZE);
revealPasswordButton.setContentDisplay(ContentDisplay.LEFT);
@@ -61,6 +61,10 @@ public class NiceSecurePasswordField extends StackPane {
disabledProperty().addListener(this::disabledChanged);
}
private FontAwesome5Icon getRevealPasswordGlyph() {
return revealPasswordButton.isSelected() ? FontAwesome5Icon.EYE_SLASH : FontAwesome5Icon.EYE;
}
private void disabledChanged(@SuppressWarnings("unused") Observable observable) {
revealPasswordButton.setSelected(false);
}
@@ -71,11 +71,9 @@ public class SecurePasswordField extends TextField {
}
public void cut() {
//not implemented by design
}
public void copy() {
//not implemented by design
}
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
@@ -43,11 +43,9 @@ public class ForgetPasswordController implements FxController {
LOG.debug("Forgot password for vault {}.", vault.getDisplayName());
confirmedResult.setValue(true);
} catch (KeychainAccessException e) {
LOG.error("Failed to delete passphrase from system keychain.", e);
LOG.error("Failed to remove entry from system keychain.", e);
confirmedResult.setValue(false);
}
} else {
LOG.warn("Keychain not supported. Doing nothing.");
}
window.close();
}
@@ -4,8 +4,6 @@ import org.cryptomator.common.vaults.Vault;
import javax.inject.Inject;
import javafx.collections.ObservableList;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@FxApplicationScoped
public class AutoUnlocker {
@@ -20,11 +18,9 @@ public class AutoUnlocker {
}
public void unlock() {
vaults.stream().filter(Vault::isLocked) //
.filter(v -> v.getVaultSettings().unlockAfterStartup().get()) //
.<CompletionStage<Void>>reduce(CompletableFuture.completedFuture(null), //
(unlockFlow, v) -> unlockFlow.handle((voit, ex) -> appWindows.startUnlockWorkflow(v, null)).thenCompose(stage -> stage), //we don't care here about the exception, logged elsewhere
(unlockChain1, unlockChain2) -> unlockChain1.handle((voit, ex) -> unlockChain2).thenCompose(stage -> stage));
vaults.stream().filter(Vault::isLocked).filter(v -> v.getVaultSettings().unlockAfterStartup().get()).forEach(v -> {
appWindows.startUnlockWorkflow(v, null);
});
}
}

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