mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-26 01:44:15 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f42c1c9ce | ||
|
|
668feb73f9 | ||
|
|
ba0569deb1 | ||
|
|
a70d8e1567 | ||
|
|
96b7a8df3f | ||
|
|
5143fdccbb | ||
|
|
3c5502955a | ||
|
|
2d3a035d9e | ||
|
|
4029f86a0d | ||
|
|
6dac00625c | ||
|
|
41b7bd54f1 | ||
|
|
f236614bd0 | ||
|
|
31c69f145e | ||
|
|
b5a00f5ebe | ||
|
|
40c4300012 | ||
|
|
5369ddb1f9 | ||
|
|
1b4117de38 | ||
|
|
3e5a7fadd0 | ||
|
|
eff2530e70 | ||
|
|
0f84d0c990 |
@@ -13,35 +13,40 @@ env:
|
|||||||
JAVA_VERSION: 19
|
JAVA_VERSION: 19
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-version:
|
|
||||||
uses: ./.github/workflows/get-version.yml
|
|
||||||
with:
|
|
||||||
version: ${{ github.event.inputs.version }}
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build AppImage
|
name: Build AppImage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [get-version]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
java-package: 'jdk+fx'
|
|
||||||
cache: 'maven'
|
cache: 'maven'
|
||||||
- name: Ensure major jfx version in pom equals in jdk
|
- id: versions
|
||||||
shell: pwsh
|
name: Apply version information
|
||||||
run: |
|
run: |
|
||||||
$jfxPomVersion = (&mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout) -split "\."
|
if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
|
||||||
$jfxJdkVersion = ((Get-Content -path "${env:JAVA_HOME}/lib/javafx.properties" | Where-Object {$_ -like 'javafx.version=*' }) -replace '.*=','') -split "\."
|
SEM_VER_STR=${GITHUB_REF##*/}
|
||||||
if ($jfxPomVersion[0] -ne $jfxJdkVersion[0]) {
|
mvn versions:set -DnewVersion=${SEM_VER_STR}
|
||||||
Write-Error "Major part of JavaFX version in pom($($jfxPomVersion[0])) does not match the version in JDK($($jfxJdkVersion[0])) "
|
elif [[ "${{ github.event.inputs.version }}" =~ [0-9]+\.[0-9]+\.[0-9]+.* ]]; then
|
||||||
exit 1
|
SEM_VER_STR="${{ github.event.inputs.version }}"
|
||||||
}
|
mvn versions:set -DnewVersion=${SEM_VER_STR}
|
||||||
- name: Set version
|
else
|
||||||
run : mvn versions:set -DnewVersion=${{ needs.get-version.outputs.semVerStr }}
|
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
|
- name: Run maven
|
||||||
run: mvn -B clean package -Pdependency-check,linux -DskipTests
|
run: mvn -B clean package -Pdependency-check,linux -DskipTests
|
||||||
- name: Patch target dir
|
- name: Patch target dir
|
||||||
@@ -55,7 +60,7 @@ jobs:
|
|||||||
--verbose
|
--verbose
|
||||||
--output runtime
|
--output runtime
|
||||||
--module-path "${JAVA_HOME}/jmods"
|
--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.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
|
||||||
--strip-native-commands
|
--strip-native-commands
|
||||||
--no-header-files
|
--no-header-files
|
||||||
--no-man-pages
|
--no-man-pages
|
||||||
@@ -64,8 +69,8 @@ jobs:
|
|||||||
- name: Prepare additional launcher
|
- name: Prepare additional launcher
|
||||||
run: envsubst '${SEMVER_STR} ${REVISION_NUM}' < dist/linux/launcher-gtk2.properties > launcher-gtk2.properties
|
run: envsubst '${SEMVER_STR} ${REVISION_NUM}' < dist/linux/launcher-gtk2.properties > launcher-gtk2.properties
|
||||||
env:
|
env:
|
||||||
SEMVER_STR: ${{ needs.get-version.outputs.semVerStr }}
|
SEMVER_STR: ${{ steps.versions.outputs.semVerStr }}
|
||||||
REVISION_NUM: ${{ needs.get-version.outputs.revNum }}
|
REVISION_NUM: ${{ steps.versions.outputs.revNum }}
|
||||||
- name: Run jpackage
|
- name: Run jpackage
|
||||||
run: >
|
run: >
|
||||||
${JAVA_HOME}/bin/jpackage
|
${JAVA_HOME}/bin/jpackage
|
||||||
@@ -79,10 +84,10 @@ jobs:
|
|||||||
--name Cryptomator
|
--name Cryptomator
|
||||||
--vendor "Skymatic GmbH"
|
--vendor "Skymatic GmbH"
|
||||||
--copyright "(C) 2016 - 2022 Skymatic GmbH"
|
--copyright "(C) 2016 - 2022 Skymatic GmbH"
|
||||||
--app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
|
--app-version "${{ steps.versions.outputs.semVerNum }}.${{ steps.versions.outputs.revNum }}"
|
||||||
--java-options "-Xss5m"
|
--java-options "-Xss5m"
|
||||||
--java-options "-Xmx256m"
|
--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 "-Dfile.encoding=\"utf-8\""
|
||||||
--java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\""
|
--java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\""
|
||||||
--java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\""
|
--java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\""
|
||||||
@@ -91,7 +96,7 @@ jobs:
|
|||||||
--java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\""
|
--java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\""
|
||||||
--java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\""
|
--java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\""
|
||||||
--java-options "-Dcryptomator.showTrayIcon=false"
|
--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
|
--add-launcher Cryptomator-gtk2=launcher-gtk2.properties
|
||||||
--resource-dir dist/linux/resources
|
--resource-dir dist/linux/resources
|
||||||
- name: Patch Cryptomator.AppDir
|
- name: Patch Cryptomator.AppDir
|
||||||
@@ -129,7 +134,7 @@ jobs:
|
|||||||
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
|
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
|
||||||
- name: Build AppImage
|
- name: Build AppImage
|
||||||
run: >
|
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'
|
-u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync'
|
||||||
--sign --sign-key=615D449FE6E6A235 --sign-args="--batch --pinentry-mode loopback"
|
--sign --sign-key=615D449FE6E6A235 --sign-args="--batch --pinentry-mode loopback"
|
||||||
- name: Create detached GPG signatures
|
- name: Create detached GPG signatures
|
||||||
|
|||||||
@@ -16,26 +16,20 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_VERSION: 19
|
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'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-version:
|
|
||||||
uses: ./.github/workflows/get-version.yml
|
|
||||||
with:
|
|
||||||
version: ${{ github.event.inputs.version }}
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build Debian Package
|
name: Build Debian Package
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [get-version]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Install build tools
|
- name: Install build tools
|
||||||
run: |
|
run: |
|
||||||
sudo add-apt-repository ppa:coffeelibs/openjdk
|
sudo add-apt-repository ppa:coffeelibs/openjdk
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install debhelper devscripts dput coffeelibs-jdk-19 libgtk2.0-0
|
sudo apt-get install debhelper devscripts dput coffeelibs-jdk-19
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
@@ -43,46 +37,34 @@ jobs:
|
|||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
cache: 'maven'
|
cache: 'maven'
|
||||||
- id: versions
|
- id: versions
|
||||||
name: Create PPA version string
|
name: Apply version information
|
||||||
run: echo "ppaVerStr=${SEM_VER_STR/-/\~}-${REVCOUNT}" >> $GITHUB_OUTPUT
|
run: |
|
||||||
env:
|
if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
|
||||||
SEM_VER_STR: ${{ needs.get-version.outputs.semVerStr }}
|
SEM_VER_STR=${GITHUB_REF##*/}
|
||||||
REVCOUNT: ${{ needs.get-version.outputs.revNum }}
|
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
|
- name: Run maven
|
||||||
run: mvn -B clean package -Pdependency-check,linux -DskipTests
|
run: mvn -B clean package -Pdependency-check,linux -DskipTests
|
||||||
- name: Download OpenJFX jmods
|
- name: Create orig.tar.gz with common/ libs/ mods/
|
||||||
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
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
mkdir jfxBaseJmodAmd64
|
|
||||||
jmod extract --dir jfxBaseJmodAmd64 jmods/amd64/javafx.base.jmod
|
|
||||||
$jfxJmodVersionAmd64 = ((Get-Content -Path "jfxBaseJmodAmd64/lib/javafx.properties" | Where-Object {$_ -like 'javafx.version=*' }) -replace '.*=','') -split "\."
|
|
||||||
mkdir jfxBaseJmodAarch64
|
|
||||||
jmod extract --dir jfxBaseJmodAarch64 jmods/aarch64/javafx.base.jmod
|
|
||||||
$jfxJmodVersionAarch64 = ((Get-Content -Path "jfxBaseJmodAarch64/lib/javafx.properties" | Where-Object {$_ -like 'javafx.version=*' }) -replace '.*=','') -split "\."
|
|
||||||
if ($jfxJmodVersionAmd64[0] -ne $jfxJmodVersionAarch64[0] ) {
|
|
||||||
Write-Error "JavaFX Jmods for aarch64 and amd64 are different major versions"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$jfxPomVersion = (&mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout) -split "\."
|
|
||||||
if ($jfxPomVersion[0] -ne $jfxJmodVersionAmd64[0]) {
|
|
||||||
Write-Error "Major part of JavaFX version in pom($($jfxPomVersion[0])) does not match the version of Jmods($($jfxJmodVersionAmd64[0])) "
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
- name: Create orig.tar.gz with common/ libs/ mods/ jmods/
|
|
||||||
run: |
|
run: |
|
||||||
mkdir pkgdir
|
mkdir pkgdir
|
||||||
cp -r target/libs pkgdir
|
cp -r target/libs pkgdir
|
||||||
cp -r target/mods pkgdir
|
cp -r target/mods pkgdir
|
||||||
cp -r jmods pkgdir
|
|
||||||
cp -r dist/linux/common/ pkgdir
|
cp -r dist/linux/common/ pkgdir
|
||||||
cp target/cryptomator-*.jar pkgdir/mods
|
cp target/cryptomator-*.jar pkgdir/mods
|
||||||
tar -cJf cryptomator_${{ steps.versions.outputs.ppaVerStr }}.orig.tar.xz -C pkgdir .
|
tar -cJf cryptomator_${{ steps.versions.outputs.ppaVerStr }}.orig.tar.xz -C pkgdir .
|
||||||
@@ -95,9 +77,9 @@ jobs:
|
|||||||
find . -name "*.jar" >> pkgdir/debian/source/include-binaries
|
find . -name "*.jar" >> pkgdir/debian/source/include-binaries
|
||||||
mv pkgdir cryptomator_${{ steps.versions.outputs.ppaVerStr }}
|
mv pkgdir cryptomator_${{ steps.versions.outputs.ppaVerStr }}
|
||||||
env:
|
env:
|
||||||
SEMVER_STR: ${{ needs.get-version.outputs.semVerStr }}
|
SEMVER_STR: ${{ steps.versions.outputs.semVerStr }}
|
||||||
VERSION_NUM: ${{ needs.get-version.outputs.semVerNum }}
|
VERSION_NUM: ${{ steps.versions.outputs.semVerNum }}
|
||||||
REVISION_NUM: ${{ needs.get-version.outputs.revNum }}
|
REVISION_NUM: ${{ steps.versions.outputs.revNum }}
|
||||||
PPA_VERSION: ${{ steps.versions.outputs.ppaVerStr }}-0ppa1
|
PPA_VERSION: ${{ steps.versions.outputs.ppaVerStr }}-0ppa1
|
||||||
- name: Prepare GPG-Agent for signing with key 615D449FE6E6A235
|
- name: Prepare GPG-Agent for signing with key 615D449FE6E6A235
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -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
|
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)
|
RESULT=$(jq -s -c "." output.json)
|
||||||
echo "result=${RESULT}" >> $GITHUB_OUTPUT
|
echo "::set-output name=result::${RESULT}"
|
||||||
env:
|
env:
|
||||||
INTERVAL: 900
|
INTERVAL: 900
|
||||||
JSON_DATA: ${{ steps.get-stats.outputs.result }}
|
JSON_DATA: ${{ steps.get-stats.outputs.result }}
|
||||||
|
|||||||
@@ -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 }}
|
|
||||||
@@ -13,15 +13,9 @@ env:
|
|||||||
JAVA_VERSION: 19
|
JAVA_VERSION: 19
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-version:
|
|
||||||
uses: ./.github/workflows/get-version.yml
|
|
||||||
with:
|
|
||||||
version: ${{ github.event.inputs.version }}
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build Cryptomator.app for ${{ matrix.output-suffix }}
|
name: Build Cryptomator.app for ${{ matrix.output-suffix }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
needs: [get-version]
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -36,26 +30,36 @@ jobs:
|
|||||||
xcode-path: '/Applications/Xcode_13.2.1.app'
|
xcode-path: '/Applications/Xcode_13.2.1.app'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
java-package: 'jdk+fx'
|
|
||||||
architecture: ${{ matrix.architecture }}
|
architecture: ${{ matrix.architecture }}
|
||||||
cache: 'maven'
|
cache: 'maven'
|
||||||
- name: Ensure major jfx version in pom equals in jdk
|
- id: versions
|
||||||
if: ${{ !contains(matrix.os, 'self-hosted') }}
|
name: Apply version information
|
||||||
shell: pwsh
|
|
||||||
run: |
|
run: |
|
||||||
$jfxPomVersion = (&mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout) -split "\."
|
if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
|
||||||
$jfxJdkVersion = ((Get-Content -path "${env:JAVA_HOME}/lib/javafx.properties" | Where-Object {$_ -like 'javafx.version=*' }) -replace '.*=','') -split "\."
|
SEM_VER_STR=${GITHUB_REF##*/}
|
||||||
if ($jfxPomVersion[0] -ne $jfxJdkVersion[0]) {
|
mvn versions:set -DnewVersion=${SEM_VER_STR}
|
||||||
Write-Error "Major part of JavaFX version in pom($($jfxPomVersion[0])) does not match the version in JDK($($jfxJdkVersion[0])) "
|
elif [[ "${{ github.event.inputs.version }}" =~ [0-9]+\.[0-9]+\.[0-9]+.* ]]; then
|
||||||
exit 1
|
SEM_VER_STR="${{ github.event.inputs.version }}"
|
||||||
}
|
mvn versions:set -DnewVersion=${SEM_VER_STR}
|
||||||
- name: Set version
|
else
|
||||||
run : mvn versions:set -DnewVersion=${{ needs.get-version.outputs.semVerStr }}
|
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
|
- name: Run maven
|
||||||
run: mvn -B clean package -Pdependency-check,mac -DskipTests
|
run: mvn -B clean package -Pdependency-check,mac -DskipTests
|
||||||
- name: Patch target dir
|
- name: Patch target dir
|
||||||
@@ -69,7 +73,7 @@ jobs:
|
|||||||
--verbose
|
--verbose
|
||||||
--output runtime
|
--output runtime
|
||||||
--module-path "${JAVA_HOME}/jmods"
|
--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.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
|
||||||
--strip-native-commands
|
--strip-native-commands
|
||||||
--no-header-files
|
--no-header-files
|
||||||
--no-man-pages
|
--no-man-pages
|
||||||
@@ -88,13 +92,13 @@ jobs:
|
|||||||
--name Cryptomator
|
--name Cryptomator
|
||||||
--vendor "Skymatic GmbH"
|
--vendor "Skymatic GmbH"
|
||||||
--copyright "(C) 2016 - 2022 Skymatic GmbH"
|
--copyright "(C) 2016 - 2022 Skymatic GmbH"
|
||||||
--app-version "${{ needs.get-version.outputs.semVerNum }}"
|
--app-version "${{ steps.versions.outputs.semVerNum }}"
|
||||||
--java-options "-Xss5m"
|
--java-options "-Xss5m"
|
||||||
--java-options "-Xmx256m"
|
--java-options "-Xmx256m"
|
||||||
--java-options "-Dfile.encoding=\"utf-8\""
|
--java-options "-Dfile.encoding=\"utf-8\""
|
||||||
--java-options "-Dapple.awt.enableTemplateImages=true"
|
--java-options "-Dapple.awt.enableTemplateImages=true"
|
||||||
--java-options "-Dsun.java2d.metal=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.logDir=\"~/Library/Logs/Cryptomator\""
|
||||||
--java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/Cryptomator/Plugins\""
|
--java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/Cryptomator/Plugins\""
|
||||||
--java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\""
|
--java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\""
|
||||||
@@ -102,7 +106,7 @@ jobs:
|
|||||||
--java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/Cryptomator/ipc.socket\""
|
--java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/Cryptomator/ipc.socket\""
|
||||||
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
|
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
|
||||||
--java-options "-Dcryptomator.showTrayIcon=true"
|
--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
|
--mac-package-identifier org.cryptomator
|
||||||
--resource-dir dist/mac/resources
|
--resource-dir dist/mac/resources
|
||||||
- name: Patch Cryptomator.app
|
- name: Patch Cryptomator.app
|
||||||
@@ -112,8 +116,8 @@ jobs:
|
|||||||
sed -i '' "s|###BUNDLE_SHORT_VERSION_STRING###|${VERSION_NO}|g" Cryptomator.app/Contents/Info.plist
|
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
|
sed -i '' "s|###BUNDLE_VERSION###|${REVISION_NO}|g" Cryptomator.app/Contents/Info.plist
|
||||||
env:
|
env:
|
||||||
VERSION_NO: ${{ needs.get-version.outputs.semVerNum }}
|
VERSION_NO: ${{ steps.versions.outputs.semVerNum }}
|
||||||
REVISION_NO: ${{ needs.get-version.outputs.revNum }}
|
REVISION_NO: ${{ steps.versions.outputs.revNum }}
|
||||||
- name: Generate license for dmg
|
- name: Generate license for dmg
|
||||||
run: >
|
run: >
|
||||||
mvn -B license:add-third-party
|
mvn -B license:add-third-party
|
||||||
@@ -147,10 +151,6 @@ jobs:
|
|||||||
CODESIGN_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_CODESIGN_TMP_KEYCHAIN_PW }}
|
CODESIGN_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_CODESIGN_TMP_KEYCHAIN_PW }}
|
||||||
- name: Codesign
|
- name: Codesign
|
||||||
run: |
|
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} {} \;
|
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
|
for JAR_PATH in `find Cryptomator.app -name "*.jar"`; do
|
||||||
if [[ `unzip -l ${JAR_PATH} | grep '.dylib\|.jnilib'` ]]; then
|
if [[ `unzip -l ${JAR_PATH} | grep '.dylib\|.jnilib'` ]]; then
|
||||||
@@ -201,7 +201,7 @@ jobs:
|
|||||||
--icon ".VolumeIcon.icns" 512 758
|
--icon ".VolumeIcon.icns" 512 758
|
||||||
Cryptomator-${VERSION_NO}-${{ matrix.output-suffix }}.dmg dmg
|
Cryptomator-${VERSION_NO}-${{ matrix.output-suffix }}.dmg dmg
|
||||||
env:
|
env:
|
||||||
VERSION_NO: ${{ needs.get-version.outputs.semVerNum }}
|
VERSION_NO: ${{ steps.versions.outputs.semVerNum }}
|
||||||
- name: Notarize .dmg
|
- name: Notarize .dmg
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: cocoalibs/xcode-notarization-action@v1
|
uses: cocoalibs/xcode-notarization-action@v1
|
||||||
@@ -212,7 +212,7 @@ jobs:
|
|||||||
team-id: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
|
team-id: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
|
||||||
xcode-path: ${{ matrix.xcode-path }}
|
xcode-path: ${{ matrix.xcode-path }}
|
||||||
- name: Add possible alpha/beta tags to installer name
|
- name: Add possible alpha/beta tags to installer name
|
||||||
run: mv Cryptomator-*.dmg Cryptomator-${{ needs.get-version.outputs.semVerStr }}-${{ matrix.output-suffix }}.dmg
|
run: mv Cryptomator-*.dmg Cryptomator-${{ steps.versions.outputs.semVerStr }}-${{ matrix.output-suffix }}.dmg
|
||||||
- name: Create detached GPG signature with key 615D449FE6E6A235
|
- name: Create detached GPG signature with key 615D449FE6E6A235
|
||||||
run: |
|
run: |
|
||||||
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
|
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ${SEM_VER_STR} == `mvn help:evaluate -Dexpression=project.version -q -DforceStdout` ]]; then
|
if [[ ${SEM_VER_STR} == `mvn help:evaluate -Dexpression=project.version -q -DforceStdout` ]]; then
|
||||||
echo "semVerStr=${SEM_VER_STR}" >> $GITHUB_OUTPUT
|
echo "::set-output name=semVerStr::${SEM_VER_STR}"
|
||||||
else
|
else
|
||||||
echo "Version not set in POM"
|
echo "Version not set in POM"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -19,37 +19,40 @@ defaults:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-version:
|
|
||||||
uses: ./.github/workflows/get-version.yml
|
|
||||||
with:
|
|
||||||
version: ${{ github.event.inputs.version }}
|
|
||||||
|
|
||||||
build-msi:
|
build-msi:
|
||||||
name: Build .msi Installer
|
name: Build .msi Installer
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
needs: [get-version]
|
|
||||||
env:
|
|
||||||
LOOPBACK_ALIAS: 'cryptomator-vault'
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: ${{ env.JAVA_DIST }}
|
distribution: ${{ env.JAVA_DIST }}
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
java-package: 'jdk+fx'
|
|
||||||
cache: ${{ env.JAVA_CACHE }}
|
cache: ${{ env.JAVA_CACHE }}
|
||||||
- name: Ensure major jfx version in pom equals in jdk
|
- id: versions
|
||||||
shell: pwsh
|
name: Apply version information
|
||||||
run: |
|
run: |
|
||||||
$jfxPomVersion = (&mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout) -split "\."
|
if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then
|
||||||
$jfxJdkVersion = ((Get-Content -path "${env:JAVA_HOME}/lib/javafx.properties" | Where-Object {$_ -like 'javafx.version=*' }) -replace '.*=','') -split "\."
|
SEM_VER_STR=${GITHUB_REF##*/}
|
||||||
if ($jfxPomVersion[0] -ne $jfxJdkVersion[0]) {
|
mvn versions:set -DnewVersion=${SEM_VER_STR}
|
||||||
Write-Error "Major part of JavaFX version in pom($($jfxPomVersion[0])) does not match the version in JDK($($jfxJdkVersion[0])) "
|
elif [[ "${{ github.event.inputs.version }}" =~ [0-9]+\.[0-9]+\.[0-9]+.* ]]; then
|
||||||
exit 1
|
SEM_VER_STR="${{ github.event.inputs.version }}"
|
||||||
}
|
mvn versions:set -DnewVersion=${SEM_VER_STR}
|
||||||
- name: Set version
|
else
|
||||||
run : mvn versions:set -DnewVersion=${{ needs.get-version.outputs.semVerStr }}
|
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
|
- name: Run maven
|
||||||
run: mvn -B clean package -Pdependency-check,win -DskipTests
|
run: mvn -B clean package -Pdependency-check,win -DskipTests
|
||||||
- name: Patch target dir
|
- name: Patch target dir
|
||||||
@@ -63,7 +66,7 @@ jobs:
|
|||||||
--verbose
|
--verbose
|
||||||
--output runtime
|
--output runtime
|
||||||
--module-path "${JAVA_HOME}/jmods"
|
--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.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
|
||||||
--strip-native-commands
|
--strip-native-commands
|
||||||
--no-header-files
|
--no-header-files
|
||||||
--no-man-pages
|
--no-man-pages
|
||||||
@@ -82,10 +85,10 @@ jobs:
|
|||||||
--name Cryptomator
|
--name Cryptomator
|
||||||
--vendor "Skymatic GmbH"
|
--vendor "Skymatic GmbH"
|
||||||
--copyright "(C) 2016 - 2022 Skymatic GmbH"
|
--copyright "(C) 2016 - 2022 Skymatic GmbH"
|
||||||
--app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
|
--app-version "${{ steps.versions.outputs.semVerNum }}.${{ steps.versions.outputs.revNum }}"
|
||||||
--java-options "-Xss5m"
|
--java-options "-Xss5m"
|
||||||
--java-options "-Xmx256m"
|
--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 "-Dfile.encoding=\"utf-8\""
|
||||||
--java-options "-Dcryptomator.logDir=\"~/AppData/Roaming/Cryptomator\""
|
--java-options "-Dcryptomator.logDir=\"~/AppData/Roaming/Cryptomator\""
|
||||||
--java-options "-Dcryptomator.pluginDir=\"~/AppData/Roaming/Cryptomator/Plugins\""
|
--java-options "-Dcryptomator.pluginDir=\"~/AppData/Roaming/Cryptomator/Plugins\""
|
||||||
@@ -93,9 +96,8 @@ jobs:
|
|||||||
--java-options "-Dcryptomator.p12Path=\"~/AppData/Roaming/Cryptomator/key.p12\""
|
--java-options "-Dcryptomator.p12Path=\"~/AppData/Roaming/Cryptomator/key.p12\""
|
||||||
--java-options "-Dcryptomator.ipcSocketPath=\"~/AppData/Roaming/Cryptomator/ipc.socket\""
|
--java-options "-Dcryptomator.ipcSocketPath=\"~/AppData/Roaming/Cryptomator/ipc.socket\""
|
||||||
--java-options "-Dcryptomator.mountPointsDir=\"~/Cryptomator\""
|
--java-options "-Dcryptomator.mountPointsDir=\"~/Cryptomator\""
|
||||||
--java-options "-Dcryptomator.loopbackAlias=\"${{ env.LOOPBACK_ALIAS }}\""
|
|
||||||
--java-options "-Dcryptomator.showTrayIcon=true"
|
--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.autoStartShellLinkName=\"Cryptomator\""
|
||||||
--java-options "-Dcryptomator.integrationsWin.keychainPaths=\"~/AppData/Roaming/Cryptomator/keychain.json\""
|
--java-options "-Dcryptomator.integrationsWin.keychainPaths=\"~/AppData/Roaming/Cryptomator/keychain.json\""
|
||||||
--resource-dir dist/win/resources
|
--resource-dir dist/win/resources
|
||||||
@@ -103,21 +105,11 @@ jobs:
|
|||||||
- name: Patch Application Directory
|
- name: Patch Application Directory
|
||||||
run: |
|
run: |
|
||||||
cp dist/win/contrib/* appdir/Cryptomator
|
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
|
- name: Fix permissions
|
||||||
run: attrib -r appdir/Cryptomator/Cryptomator.exe
|
run: attrib -r appdir/Cryptomator/Cryptomator.exe
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: Codesign
|
- name: Codesign
|
||||||
uses: skymatic/code-sign-action@v2
|
uses: skymatic/code-sign-action@v1
|
||||||
with:
|
with:
|
||||||
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
|
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
|
||||||
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
|
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
|
||||||
@@ -148,7 +140,7 @@ jobs:
|
|||||||
--name Cryptomator
|
--name Cryptomator
|
||||||
--vendor "Skymatic GmbH"
|
--vendor "Skymatic GmbH"
|
||||||
--copyright "(C) 2016 - 2022 Skymatic GmbH"
|
--copyright "(C) 2016 - 2022 Skymatic GmbH"
|
||||||
--app-version "${{ needs.get-version.outputs.semVerNum }}"
|
--app-version "${{ steps.versions.outputs.semVerNum }}"
|
||||||
--win-menu
|
--win-menu
|
||||||
--win-dir-chooser
|
--win-dir-chooser
|
||||||
--win-shortcut-prompt
|
--win-shortcut-prompt
|
||||||
@@ -160,7 +152,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
JP_WIXWIZARD_RESOURCES: ${{ github.workspace }}/dist/win/resources # requires abs path, used in resources/main.wxs
|
JP_WIXWIZARD_RESOURCES: ${{ github.workspace }}/dist/win/resources # requires abs path, used in resources/main.wxs
|
||||||
- name: Codesign MSI
|
- name: Codesign MSI
|
||||||
uses: skymatic/code-sign-action@v2
|
uses: skymatic/code-sign-action@v1
|
||||||
with:
|
with:
|
||||||
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
|
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
|
||||||
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
|
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
|
||||||
@@ -169,7 +161,7 @@ jobs:
|
|||||||
timestampUrl: 'http://timestamp.digicert.com'
|
timestampUrl: 'http://timestamp.digicert.com'
|
||||||
folder: installer
|
folder: installer
|
||||||
- name: Add possible alpha/beta tags to installer name
|
- 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
|
- name: Create detached GPG signature with key 615D449FE6E6A235
|
||||||
run: |
|
run: |
|
||||||
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
|
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
|
||||||
@@ -194,20 +186,23 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
*.msi
|
*.msi
|
||||||
*.asc
|
*.asc
|
||||||
|
outputs:
|
||||||
|
semVerNum: ${{ steps.versions.outputs.semVerNum }}
|
||||||
|
semVerStr: ${{ steps.versions.outputs.semVerStr }}
|
||||||
|
revNum: ${{ steps.versions.outputs.revNum }}
|
||||||
|
|
||||||
call-winget-flow:
|
call-winget-flow:
|
||||||
needs: [get-version, build-msi]
|
needs: [build-msi]
|
||||||
if: github.event.action == 'published' && needs.get-version.outputs.type == 'stable'
|
if: github.event.action == 'published'
|
||||||
uses: ./.github/workflows/winget.yml
|
uses: ./.github/workflows/winget.yml
|
||||||
with:
|
with:
|
||||||
releaseTag: ${{ github.event.release.tag_name }}
|
releaseTag: ${{ github.event.release.tag_name }}
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
|
|
||||||
build-exe:
|
build-exe:
|
||||||
name: Build .exe installer
|
name: Build .exe installer
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
needs: [get-version, build-msi]
|
needs: [build-msi]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Download .msi
|
- name: Download .msi
|
||||||
@@ -243,7 +238,7 @@ jobs:
|
|||||||
"${WIX}/bin/candle.exe" dist/win/bundle/bundleWithWinfsp.wxs
|
"${WIX}/bin/candle.exe" dist/win/bundle/bundleWithWinfsp.wxs
|
||||||
-ext WixBalExtension
|
-ext WixBalExtension
|
||||||
-out dist/win/bundle/
|
-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"
|
-dBundleVendor="Skymatic GmbH"
|
||||||
-dBundleCopyright="(C) 2016 - 2022 Skymatic GmbH"
|
-dBundleCopyright="(C) 2016 - 2022 Skymatic GmbH"
|
||||||
-dAboutUrl="https://cryptomator.org"
|
-dAboutUrl="https://cryptomator.org"
|
||||||
@@ -260,7 +255,7 @@ jobs:
|
|||||||
-ib installer/unsigned/Cryptomator-Installer.exe
|
-ib installer/unsigned/Cryptomator-Installer.exe
|
||||||
-o tmp/engine.exe
|
-o tmp/engine.exe
|
||||||
- name: Codesign burn engine
|
- name: Codesign burn engine
|
||||||
uses: skymatic/code-sign-action@v2
|
uses: skymatic/code-sign-action@v1
|
||||||
with:
|
with:
|
||||||
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
|
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
|
||||||
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
|
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
|
||||||
@@ -274,7 +269,7 @@ jobs:
|
|||||||
-ab tmp/engine.exe installer/unsigned/Cryptomator-Installer.exe
|
-ab tmp/engine.exe installer/unsigned/Cryptomator-Installer.exe
|
||||||
-o installer/Cryptomator-Installer.exe
|
-o installer/Cryptomator-Installer.exe
|
||||||
- name: Codesign EXE
|
- name: Codesign EXE
|
||||||
uses: skymatic/code-sign-action@v2
|
uses: skymatic/code-sign-action@v1
|
||||||
with:
|
with:
|
||||||
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
|
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
|
||||||
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
|
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
|
||||||
@@ -283,7 +278,7 @@ jobs:
|
|||||||
timestampUrl: 'http://timestamp.digicert.com'
|
timestampUrl: 'http://timestamp.digicert.com'
|
||||||
folder: installer
|
folder: installer
|
||||||
- name: Add possible alpha/beta tags to installer name
|
- 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
|
- name: Create detached GPG signature with key 615D449FE6E6A235
|
||||||
run: |
|
run: |
|
||||||
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
|
echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import
|
||||||
|
|||||||
Generated
+1
-1
@@ -45,7 +45,7 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="JavacSettings">
|
<component name="JavacSettings">
|
||||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||||
<module name="cryptomator" options="-Adagger.fastInit=enabled -Adagger.formatGeneratedSource=enabled" />
|
<module name="cryptomator" options="-Adagger.fastInit=enabled -Adagger.formatGeneratedSource=enabled --enable-preview" />
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
|
<configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="cryptomator" />
|
<module name="cryptomator" />
|
||||||
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath="~/.config/Cryptomator/settings.json" -Dcryptomator.p12Path="~/.config/Cryptomator/key.p12" -Dcryptomator.ipcSocketPath="~/.config/Cryptomator/ipc.socket" -Dcryptomator.logDir="~/.local/share/Cryptomator/logs" -Dcryptomator.pluginDir="~/.local/share/Cryptomator/plugins" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator/mnt" -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
|
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath="~/.config/Cryptomator/settings.json" -Dcryptomator.p12Path="~/.config/Cryptomator/key.p12" -Dcryptomator.ipcSocketPath="~/.config/Cryptomator/ipc.socket" -Dcryptomator.logDir="~/.local/share/Cryptomator/logs" -Dcryptomator.pluginDir="~/.local/share/Cryptomator/plugins" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator/mnt" -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
|
<configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="cryptomator" />
|
<module name="cryptomator" />
|
||||||
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath="~/.config/Cryptomator-Dev/settings.json" -Dcryptomator.p12Path="~/.config/Cryptomator-Dev/key.p12" -Dcryptomator.ipcSocketPath="~/.config/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/.local/share/Cryptomator-Dev/logs" -Dcryptomator.pluginDir="~/.local/share/Cryptomator-Dev/plugins" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator-Dev/mnt" -Dcryptomator.showTrayIcon=true -Dfuse.experimental="true" -Xss20m -Xmx512m" />
|
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath="~/.config/Cryptomator-Dev/settings.json" -Dcryptomator.p12Path="~/.config/Cryptomator-Dev/key.p12" -Dcryptomator.ipcSocketPath="~/.config/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/.local/share/Cryptomator-Dev/logs" -Dcryptomator.pluginDir="~/.local/share/Cryptomator-Dev/plugins" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator-Dev/mnt" -Dcryptomator.showTrayIcon=true -Dfuse.experimental="true" -Xss20m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
|
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="cryptomator" />
|
<module name="cryptomator" />
|
||||||
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" -Dcryptomator.ipcSocketPath="~/AppData/Roaming/Cryptomator/ipc.socket" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" -Dcryptomator.pluginDir="~/AppData/Roaming/Cryptomator/Plugins" -Dcryptomator.integrationsWin.keychainPaths="~/AppData/Roaming/Cryptomator/keychain.json" -Dcryptomator.p12Path="~/AppData/Roaming/Cryptomator/key.p12" -Dcryptomator.mountPointsDir="~/Cryptomator" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
|
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" -Dcryptomator.ipcSocketPath="~/AppData/Roaming/Cryptomator/ipc.socket" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" -Dcryptomator.pluginDir="~/AppData/Roaming/Cryptomator/Plugins" -Dcryptomator.integrationsWin.keychainPaths="~/AppData/Roaming/Cryptomator/keychain.json" -Dcryptomator.p12Path="~/AppData/Roaming/Cryptomator/key.p12" -Dcryptomator.mountPointsDir="~/Cryptomator" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.win" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
|
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="cryptomator" />
|
<module name="cryptomator" />
|
||||||
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator-Dev/settings.json" -Dcryptomator.ipcSocketPath="~/AppData/Roaming/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator-Dev" -Dcryptomator.pluginDir="~/AppData/Roaming/Cryptomator-Dev/Plugins" -Dcryptomator.integrationsWin.keychainPaths="~/AppData/Roaming/Cryptomator-Dev/keychain.json" -Dcryptomator.p12Path="~/AppData/Roaming/Cryptomator-Dev/key.p12" -Dcryptomator.mountPointsDir="~/Cryptomator-Dev" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
|
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator-Dev/settings.json" -Dcryptomator.ipcSocketPath="~/AppData/Roaming/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator-Dev" -Dcryptomator.pluginDir="~/AppData/Roaming/Cryptomator-Dev/Plugins" -Dcryptomator.integrationsWin.keychainPaths="~/AppData/Roaming/Cryptomator-Dev/keychain.json" -Dcryptomator.p12Path="~/AppData/Roaming/Cryptomator-Dev/key.p12" -Dcryptomator.mountPointsDir="~/Cryptomator-Dev" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m --enable-preview --enable-native-access=org.cryptomator.jfuse.win" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
</envs>
|
</envs>
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="cryptomator" />
|
<module name="cryptomator" />
|
||||||
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.p12Path="~/Library/Application Support/Cryptomator/key.p12" -Dcryptomator.ipcSocketPath="~/Library/Application Support/Cryptomator/ipc.socket" -Dcryptomator.logDir="~/Library/Logs/Cryptomator" -Dcryptomator.pluginDir="~/Library/Application Support/Cryptomator/Plugins" -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Xss2m -Xmx512m -ea" />
|
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.p12Path="~/Library/Application Support/Cryptomator/key.p12" -Dcryptomator.ipcSocketPath="~/Library/Application Support/Cryptomator/ipc.socket" -Dcryptomator.logDir="~/Library/Logs/Cryptomator" -Dcryptomator.pluginDir="~/Library/Application Support/Cryptomator/Plugins" -Dcryptomator.mountPointsDir="~/Cryptomator" -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
</envs>
|
</envs>
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="cryptomator" />
|
<module name="cryptomator" />
|
||||||
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator-Dev/settings.json" -Dcryptomator.p12Path="~/Library/Application Support/Cryptomator-Dev/key.p12" -Dcryptomator.ipcSocketPath="~/Library/Application Support/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/Library/Logs/Cryptomator-Dev" -Dcryptomator.pluginDir="~/Library/Application Support/Cryptomator-Dev/Plugins" -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Xss2m -Xmx512m -ea" />
|
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator-Dev/settings.json" -Dcryptomator.p12Path="~/Library/Application Support/Cryptomator-Dev/key.p12" -Dcryptomator.ipcSocketPath="~/Library/Application Support/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/Library/Logs/Cryptomator-Dev" -Dcryptomator.pluginDir="~/Library/Application Support/Cryptomator-Dev/Plugins" -Dcryptomator.mountPointsDir="~/Cryptomator" -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
|
|||||||
- File names get encrypted
|
- File names get encrypted
|
||||||
- Folder structure gets obfuscated
|
- Folder structure gets obfuscated
|
||||||
- Use as many vaults in your Dropbox as you want, each having individual passwords
|
- 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
|
### Privacy
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ For more information on the security details visit [cryptomator.org](https://doc
|
|||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
* JDK 19 (e.g. temurin)
|
* JDK 17 (e.g. temurin)
|
||||||
* Maven 3
|
* Maven 3
|
||||||
|
|
||||||
### Run Maven
|
### Run Maven
|
||||||
@@ -99,6 +99,10 @@ mvn clean install
|
|||||||
|
|
||||||
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.
|
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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -66,8 +66,6 @@
|
|||||||
</content_rating>
|
</content_rating>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
<release date="2022-12-14" version="1.6.17"/>
|
|
||||||
<release date="2022-12-06" version="1.6.16"/>
|
|
||||||
<release date="2022-10-06" version="1.6.15"/>
|
<release date="2022-10-06" version="1.6.15"/>
|
||||||
<release date="2022-08-31" version="1.6.14"/>
|
<release date="2022-08-31" version="1.6.14"/>
|
||||||
<release date="2022-07-27" version="1.6.12"/>
|
<release date="2022-07-27" version="1.6.12"/>
|
||||||
|
|||||||
Vendored
+1
-1
@@ -2,7 +2,7 @@ Source: cryptomator
|
|||||||
Maintainer: Cryptobot <releases@cryptomator.org>
|
Maintainer: Cryptobot <releases@cryptomator.org>
|
||||||
Section: utils
|
Section: utils
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Build-Depends: debhelper (>=10), coffeelibs-jdk-19, libgtk2.0-0
|
Build-Depends: debhelper (>=10), coffeelibs-jdk-19
|
||||||
Standards-Version: 4.5.0
|
Standards-Version: 4.5.0
|
||||||
Homepage: https://cryptomator.org
|
Homepage: https://cryptomator.org
|
||||||
Vcs-Git: https://github.com/cryptomator/cryptomator.git
|
Vcs-Git: https://github.com/cryptomator/cryptomator.git
|
||||||
|
|||||||
Vendored
+1
-8
@@ -5,12 +5,6 @@
|
|||||||
#export DH_VERBOSE=1
|
#export DH_VERBOSE=1
|
||||||
|
|
||||||
JAVA_HOME = /usr/lib/jvm/java-19-coffeelibs
|
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 $@
|
dh $@
|
||||||
@@ -26,8 +20,7 @@ override_dh_auto_build:
|
|||||||
ln -s ../common/org.cryptomator.Cryptomator512.png resources/cryptomator.png
|
ln -s ../common/org.cryptomator.Cryptomator512.png resources/cryptomator.png
|
||||||
$(JAVA_HOME)/bin/jlink \
|
$(JAVA_HOME)/bin/jlink \
|
||||||
--output runtime \
|
--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,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
|
||||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
|
|
||||||
--strip-native-commands \
|
--strip-native-commands \
|
||||||
--no-header-files \
|
--no-header-files \
|
||||||
--no-man-pages \
|
--no-man-pages \
|
||||||
|
|||||||
Vendored
+1
-3
@@ -9,9 +9,8 @@ SET ABOUT_URL="https://cryptomator.org"
|
|||||||
SET UPDATE_URL="https://cryptomator.org/downloads/"
|
SET UPDATE_URL="https://cryptomator.org/downloads/"
|
||||||
SET HELP_URL="https://cryptomator.org/contact/"
|
SET HELP_URL="https://cryptomator.org/contact/"
|
||||||
SET MODULE_AND_MAIN_CLASS="org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator"
|
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%^
|
-AppName %APPNAME%^
|
||||||
-MainJarGlob "%MAIN_JAR_GLOB%"^
|
-MainJarGlob "%MAIN_JAR_GLOB%"^
|
||||||
-ModuleAndMainClass "%MODULE_AND_MAIN_CLASS%"^
|
-ModuleAndMainClass "%MODULE_AND_MAIN_CLASS%"^
|
||||||
@@ -21,5 +20,4 @@ powershell -NoLogo -ExecutionPolicy Unrestricted -Command .\build.ps1^
|
|||||||
-AboutUrl "%ABOUT_URL%"^
|
-AboutUrl "%ABOUT_URL%"^
|
||||||
-HelpUrl "%HELP_URL%"^
|
-HelpUrl "%HELP_URL%"^
|
||||||
-UpdateUrl "%UPDATE_URL%"^
|
-UpdateUrl "%UPDATE_URL%"^
|
||||||
-LoopbackAlias "%LOOPBACK_ALIAS%"^
|
|
||||||
-Clean 1
|
-Clean 1
|
||||||
Vendored
-10
@@ -8,7 +8,6 @@ Param(
|
|||||||
[Parameter(Mandatory, HelpMessage="Please provide a help url")][string] $HelpUrl,
|
[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 update url")][string] $UpdateUrl,
|
||||||
[Parameter(Mandatory, HelpMessage="Please provide an about url")][string] $AboutUrl,
|
[Parameter(Mandatory, HelpMessage="Please provide an about url")][string] $AboutUrl,
|
||||||
[Parameter(Mandatory, HelpMessage="Please provide an alias for localhost")][string] $LoopbackAlias,
|
|
||||||
[bool] $clean
|
[bool] $clean
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,7 +85,6 @@ if ($clean -and (Test-Path -Path $appPath)) {
|
|||||||
--java-options "-Dcryptomator.ipcSocketPath=`"~/AppData/Roaming/$AppName/ipc.socket`"" `
|
--java-options "-Dcryptomator.ipcSocketPath=`"~/AppData/Roaming/$AppName/ipc.socket`"" `
|
||||||
--java-options "-Dcryptomator.p12Path=`"~/AppData/Roaming/$AppName/key.p12`"" `
|
--java-options "-Dcryptomator.p12Path=`"~/AppData/Roaming/$AppName/key.p12`"" `
|
||||||
--java-options "-Dcryptomator.mountPointsDir=`"~/$AppName`"" `
|
--java-options "-Dcryptomator.mountPointsDir=`"~/$AppName`"" `
|
||||||
--java-options "-Dcryptomator.loopbackAlias=`"$LoopbackAlias`"" `
|
|
||||||
--java-options "-Dcryptomator.integrationsWin.autoStartShellLinkName=`"$AppName`"" `
|
--java-options "-Dcryptomator.integrationsWin.autoStartShellLinkName=`"$AppName`"" `
|
||||||
--java-options "-Dcryptomator.integrationsWin.keychainPaths=`"~/AppData/Roaming/$AppName/keychain.json`"" `
|
--java-options "-Dcryptomator.integrationsWin.keychainPaths=`"~/AppData/Roaming/$AppName/keychain.json`"" `
|
||||||
--java-options "-Dcryptomator.showTrayIcon=true" `
|
--java-options "-Dcryptomator.showTrayIcon=true" `
|
||||||
@@ -107,14 +105,6 @@ if ($clean -and (Test-Path -Path $appPath)) {
|
|||||||
# patch app dir
|
# patch app dir
|
||||||
Copy-Item "contrib\*" -Destination "$AppName"
|
Copy-Item "contrib\*" -Destination "$AppName"
|
||||||
attrib -r "$AppName\$AppName.exe"
|
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
|
# create .msi
|
||||||
$Env:JP_WIXWIZARD_RESOURCES = "$buildDir\resources"
|
$Env:JP_WIXWIZARD_RESOURCES = "$buildDir\resources"
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
@echo off
|
@echo off
|
||||||
:: Default values for Cryptomator builds
|
:: Default values for Cryptomator builds
|
||||||
::REPLACE ME
|
SET LOOPBACK_ALIAS="cryptomator-vault"
|
||||||
|
|
||||||
cd %~dp0
|
cd %~dp0
|
||||||
powershell -NoLogo -NonInteractive -ExecutionPolicy Unrestricted -Command .\patchWebDAV.ps1^
|
powershell -NoLogo -NonInteractive -ExecutionPolicy Unrestricted -Command .\patchWebDAV.ps1^
|
||||||
|
|||||||
Vendored
+4
-9
@@ -23,11 +23,6 @@
|
|||||||
<?define JpUpgradeVersionOnlyDetectDowngrade="yes"?>
|
<?define JpUpgradeVersionOnlyDetectDowngrade="yes"?>
|
||||||
<?endif?>
|
<?endif?>
|
||||||
|
|
||||||
<!-- Cryptomator defaults -->
|
|
||||||
<?define IconFileEncryptedData= "Cryptomator-Vault.ico" ?>
|
|
||||||
<?define ProgIdContentType= "application/vnd.cryptomator.encrypted" ?>
|
|
||||||
<?define CloseApplicationTarget= "cryptomator.exe" ?>
|
|
||||||
|
|
||||||
<?include $(var.JpConfigDir)/overrides.wxi ?>
|
<?include $(var.JpConfigDir)/overrides.wxi ?>
|
||||||
|
|
||||||
<Product
|
<Product
|
||||||
@@ -70,16 +65,16 @@
|
|||||||
<CustomAction Id="JpDisallowDowngrade" Error="!(loc.DowngradeErrorMessage)" />
|
<CustomAction Id="JpDisallowDowngrade" Error="!(loc.DowngradeErrorMessage)" />
|
||||||
<?endif?>
|
<?endif?>
|
||||||
|
|
||||||
<?ifndef SkipCryptomatorLegacyCheck ?>
|
|
||||||
<!-- Block installation if innosetup entry of Cryptomator is found -->
|
<!-- Looking for legacy Cryptomator versions-->
|
||||||
<Property Id="OLDEXEINSTALLER">
|
<Property Id="OLDEXEINSTALLER">
|
||||||
<RegistrySearch Id="InnoSetupInstallation" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\Cryptomator_is1" Type="raw" Name="DisplayName" />
|
<RegistrySearch Id="InnoSetupInstallation" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\Cryptomator_is1" Type="raw" Name="DisplayName" />
|
||||||
</Property>
|
</Property>
|
||||||
|
<!-- Block installation if innosetup entry of Cryptomator is found -->
|
||||||
<!-- TODO: localize -->
|
<!-- 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.">
|
<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]]>
|
<![CDATA[Installed OR NOT OLDEXEINSTALLER]]>
|
||||||
</Condition>
|
</Condition>
|
||||||
<?endif?>
|
|
||||||
<!-- Cryptomator uses UNIX Sockets, which are supported starting with Windows 10 v1803-->
|
<!-- Cryptomator uses UNIX Sockets, which are supported starting with Windows 10 v1803-->
|
||||||
<Property Id="WINDOWSBUILDNUMBER" Secure="yes">
|
<Property Id="WINDOWSBUILDNUMBER" Secure="yes">
|
||||||
<RegistrySearch Id="BuildNumberSearch" Root="HKLM" Key="SOFTWARE\Microsoft\Windows NT\CurrentVersion" Name="CurrentBuildNumber" Type="raw" />
|
<RegistrySearch Id="BuildNumberSearch" Root="HKLM" Key="SOFTWARE\Microsoft\Windows NT\CurrentVersion" Name="CurrentBuildNumber" Type="raw" />
|
||||||
@@ -91,7 +86,7 @@
|
|||||||
<!-- Non-Opening ProgID -->
|
<!-- Non-Opening ProgID -->
|
||||||
<DirectoryRef Id="INSTALLDIR">
|
<DirectoryRef Id="INSTALLDIR">
|
||||||
<Component Win64="yes" Id="nonStartingProgID" >
|
<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">
|
<ProgId Id="$(var.JpAppName).Encrypted.1" Description="$(var.JpAppName) Encrypted Data" Icon="IconFileForEncryptedData" IconIndex="0">
|
||||||
<Extension Id="c9r" Advertise="no" ContentType="$(var.ProgIdContentType)">
|
<Extension Id="c9r" Advertise="no" ContentType="$(var.ProgIdContentType)">
|
||||||
<MIME ContentType="$(var.ProgIdContentType)" Default="yes"></MIME>
|
<MIME ContentType="$(var.ProgIdContentType)" Default="yes"></MIME>
|
||||||
|
|||||||
Vendored
+6
-17
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Stub by design -->
|
<!-- Stub by design -->
|
||||||
|
|
||||||
<!-- jPackage Section
|
<!--
|
||||||
overrides.wxi is a placeholder to set/alter WiX variables referenced from default
|
overrides.wxi is a placeholder to set/alter WiX variables referenced from default
|
||||||
main.wxs file.
|
main.wxs file.
|
||||||
|
|
||||||
@@ -30,21 +30,10 @@ Should be defined to enable upgrades and undefined to disable upgrades.
|
|||||||
Default value is `yes`.
|
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:
|
<!-- Close Application util -->
|
||||||
- IconFileEncryptedData
|
<?define CloseApplicationTarget= "cryptomator.exe" ?>
|
||||||
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.
|
|
||||||
-->
|
|
||||||
<Include/>
|
<Include/>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>cryptomator</artifactId>
|
<artifactId>cryptomator</artifactId>
|
||||||
<version>1.6.17</version>
|
<version>1.7.0</version>
|
||||||
<name>Cryptomator Desktop App</name>
|
<name>Cryptomator Desktop App</name>
|
||||||
|
|
||||||
<organization>
|
<organization>
|
||||||
@@ -24,18 +24,19 @@
|
|||||||
<project.jdk.version>19</project.jdk.version>
|
<project.jdk.version>19</project.jdk.version>
|
||||||
|
|
||||||
<!-- Group IDs of jars that need to stay on the class path for now -->
|
<!-- Group IDs of jars that need to stay on the class path for now -->
|
||||||
<nonModularGroupIds>com.github.serceman,com.github.jnr,org.ow2.asm,net.java.dev.jna,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh</nonModularGroupIds>
|
<!-- Check progress on https://github.com/swiesend/secret-service/issues/31 to remove hypfvieh, swiesend and jnr -->
|
||||||
|
<nonModularGroupIds>com.github.jnr,org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh</nonModularGroupIds>
|
||||||
|
|
||||||
<!-- cryptomator dependencies -->
|
<!-- cryptomator dependencies -->
|
||||||
<cryptomator.cryptolib.version>2.1.1</cryptomator.cryptolib.version>
|
<cryptomator.cryptolib.version>2.1.0-rc1</cryptomator.cryptolib.version>
|
||||||
<cryptomator.cryptofs.version>2.5.3</cryptomator.cryptofs.version>
|
<cryptomator.cryptofs.version>2.4.5</cryptomator.cryptofs.version>
|
||||||
<cryptomator.integrations.version>1.2.0-beta1</cryptomator.integrations.version>
|
<cryptomator.integrations.version>1.2.0-beta2</cryptomator.integrations.version>
|
||||||
<cryptomator.integrations.win.version>1.1.2</cryptomator.integrations.win.version>
|
<cryptomator.integrations.win.version>1.1.2</cryptomator.integrations.win.version>
|
||||||
<cryptomator.integrations.mac.version>1.1.2</cryptomator.integrations.mac.version>
|
<cryptomator.integrations.mac.version>1.1.2</cryptomator.integrations.mac.version>
|
||||||
<cryptomator.integrations.linux.version>1.1.0</cryptomator.integrations.linux.version>
|
<cryptomator.integrations.linux.version>1.1.0</cryptomator.integrations.linux.version>
|
||||||
<cryptomator.fuse.version>1.3.4</cryptomator.fuse.version>
|
<cryptomator.fuse.version>2.0.0-beta1</cryptomator.fuse.version>
|
||||||
<cryptomator.dokany.version>1.3.3</cryptomator.dokany.version>
|
<cryptomator.dokany.version>2.0.0-beta1</cryptomator.dokany.version>
|
||||||
<cryptomator.webdav.version>1.2.8</cryptomator.webdav.version>
|
<cryptomator.webdav.version>2.0.0-beta1</cryptomator.webdav.version>
|
||||||
|
|
||||||
<!-- 3rd party dependencies -->
|
<!-- 3rd party dependencies -->
|
||||||
<commons-lang3.version>3.12.0</commons-lang3.version>
|
<commons-lang3.version>3.12.0</commons-lang3.version>
|
||||||
@@ -58,14 +59,13 @@
|
|||||||
|
|
||||||
<!-- build-time dependencies -->
|
<!-- build-time dependencies -->
|
||||||
<jetbrains.annotations.version>23.0.0</jetbrains.annotations.version>
|
<jetbrains.annotations.version>23.0.0</jetbrains.annotations.version>
|
||||||
<dependency-check.version>7.4.0</dependency-check.version>
|
<dependency-check.version>7.2.1</dependency-check.version>
|
||||||
<jacoco.version>0.8.8</jacoco.version>
|
<jacoco.version>0.8.8</jacoco.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- Cryptomator Libs -->
|
<!-- Cryptomator Libs -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<!-- needed due to https://github.com/cryptomator/cryptolib/issues/34-->
|
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>cryptolib</artifactId>
|
<artifactId>cryptolib</artifactId>
|
||||||
<version>${cryptomator.cryptolib.version}</version>
|
<version>${cryptomator.cryptolib.version}</version>
|
||||||
@@ -314,6 +314,7 @@
|
|||||||
<compilerArgs>
|
<compilerArgs>
|
||||||
<arg>-Adagger.fastInit=enabled</arg>
|
<arg>-Adagger.fastInit=enabled</arg>
|
||||||
<arg>-Adagger.formatGeneratedSource=enabled</arg>
|
<arg>-Adagger.formatGeneratedSource=enabled</arg>
|
||||||
|
<arg>--enable-preview</arg>
|
||||||
</compilerArgs>
|
</compilerArgs>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
@@ -372,7 +373,6 @@
|
|||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-dependency-plugin</artifactId>
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
<executions>
|
<executions>
|
||||||
<!-- sort jars into two buckets (classpath and modulepath). exclude openjfx, which gets jlinked separately -->
|
|
||||||
<execution>
|
<execution>
|
||||||
<id>copy-mods</id>
|
<id>copy-mods</id>
|
||||||
<phase>prepare-package</phase>
|
<phase>prepare-package</phase>
|
||||||
@@ -382,7 +382,7 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<includeScope>runtime</includeScope>
|
<includeScope>runtime</includeScope>
|
||||||
<outputDirectory>${project.build.directory}/mods</outputDirectory>
|
<outputDirectory>${project.build.directory}/mods</outputDirectory>
|
||||||
<excludeGroupIds>org.openjfx,${nonModularGroupIds}</excludeGroupIds>
|
<excludeGroupIds>${nonModularGroupIds}</excludeGroupIds>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
<execution>
|
<execution>
|
||||||
|
|||||||
@@ -10,19 +10,17 @@ import dagger.Module;
|
|||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
import org.cryptomator.common.keychain.KeychainModule;
|
import org.cryptomator.common.keychain.KeychainModule;
|
||||||
|
import org.cryptomator.common.mount.MountModule;
|
||||||
import org.cryptomator.common.settings.Settings;
|
import org.cryptomator.common.settings.Settings;
|
||||||
import org.cryptomator.common.settings.SettingsProvider;
|
import org.cryptomator.common.settings.SettingsProvider;
|
||||||
import org.cryptomator.common.vaults.VaultComponent;
|
import org.cryptomator.common.vaults.VaultComponent;
|
||||||
import org.cryptomator.common.vaults.VaultListModule;
|
import org.cryptomator.common.vaults.VaultListModule;
|
||||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import javafx.beans.binding.Binding;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
@@ -34,7 +32,7 @@ import java.util.concurrent.SynchronousQueue;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class})
|
@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class, MountModule.class})
|
||||||
public abstract class CommonsModule {
|
public abstract class CommonsModule {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
|
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
|
||||||
@@ -138,13 +136,4 @@ public abstract class CommonsModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
static WebDavServer provideWebDavServer(ObservableValue<InetSocketAddress> serverSocketAddressBinding) {
|
|
||||||
WebDavServer server = WebDavServer.create();
|
|
||||||
// no need to unsubscribe eventually, because server is a singleton
|
|
||||||
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ public class Environment {
|
|||||||
private static final String KEYCHAIN_PATHS_PROP_NAME = "cryptomator.integrationsWin.keychainPaths";
|
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 P12_PATH_PROP_NAME = "cryptomator.p12Path";
|
||||||
private static final String LOG_DIR_PROP_NAME = "cryptomator.logDir";
|
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 MOUNTPOINT_DIR_PROP_NAME = "cryptomator.mountPointsDir";
|
||||||
private static final String MIN_PW_LENGTH_PROP_NAME = "cryptomator.minPwLength";
|
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 APP_VERSION_PROP_NAME = "cryptomator.appVersion";
|
||||||
@@ -46,7 +45,6 @@ public class Environment {
|
|||||||
logCryptomatorSystemProperty(IPC_SOCKET_PATH_PROP_NAME);
|
logCryptomatorSystemProperty(IPC_SOCKET_PATH_PROP_NAME);
|
||||||
logCryptomatorSystemProperty(KEYCHAIN_PATHS_PROP_NAME);
|
logCryptomatorSystemProperty(KEYCHAIN_PATHS_PROP_NAME);
|
||||||
logCryptomatorSystemProperty(LOG_DIR_PROP_NAME);
|
logCryptomatorSystemProperty(LOG_DIR_PROP_NAME);
|
||||||
logCryptomatorSystemProperty(LOOPBACK_ALIAS_PROP_NAME);
|
|
||||||
logCryptomatorSystemProperty(PLUGIN_DIR_PROP_NAME);
|
logCryptomatorSystemProperty(PLUGIN_DIR_PROP_NAME);
|
||||||
logCryptomatorSystemProperty(MOUNTPOINT_DIR_PROP_NAME);
|
logCryptomatorSystemProperty(MOUNTPOINT_DIR_PROP_NAME);
|
||||||
logCryptomatorSystemProperty(MIN_PW_LENGTH_PROP_NAME);
|
logCryptomatorSystemProperty(MIN_PW_LENGTH_PROP_NAME);
|
||||||
@@ -92,10 +90,6 @@ public class Environment {
|
|||||||
return getPath(LOG_DIR_PROP_NAME).map(this::replaceHomeDir);
|
return getPath(LOG_DIR_PROP_NAME).map(this::replaceHomeDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<String> getLoopbackAlias() {
|
|
||||||
return Optional.ofNullable(System.getProperty(LOOPBACK_ALIAS_PROP_NAME));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Path> getPluginDir() {
|
public Optional<Path> getPluginDir() {
|
||||||
return getPath(PLUGIN_DIR_PROP_NAME).map(this::replaceHomeDir);
|
return getPath(PLUGIN_DIR_PROP_NAME).map(this::replaceHomeDir);
|
||||||
}
|
}
|
||||||
@@ -118,13 +112,22 @@ public class Environment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getMinPwLength() {
|
public int getMinPwLength() {
|
||||||
return Integer.getInteger(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH);
|
return getInt(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean showTrayIcon() {
|
public boolean showTrayIcon() {
|
||||||
return Boolean.getBoolean(TRAY_ICON_PROP_NAME);
|
return Boolean.getBoolean(TRAY_ICON_PROP_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private Optional<Path> getPath(String propertyName) {
|
||||||
String value = System.getProperty(propertyName);
|
String value = System.getProperty(propertyName);
|
||||||
return Optional.ofNullable(value).map(Paths::get);
|
return Optional.ofNullable(value).map(Paths::get);
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package org.cryptomator.common.mount;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import org.cryptomator.common.settings.Settings;
|
||||||
|
import org.cryptomator.integrations.mount.MountService;
|
||||||
|
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
public class MountModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
static List<MountService> provideSupportedMountServices() {
|
||||||
|
return MountService.get().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
static ObservableValue<MountService> provideMountService(Settings settings, List<MountService> serviceImpls) {
|
||||||
|
return settings.mountService().map(desiredServiceImpl -> {
|
||||||
|
var fallbackProvider = serviceImpls.stream().findFirst().orElse(null);
|
||||||
|
return serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(desiredServiceImpl)).findAny().orElse(fallbackProvider);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the accompanying LICENSE file.
|
||||||
|
*******************************************************************************/
|
||||||
|
package org.cryptomator.common.mount;
|
||||||
|
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public final class WindowsDriveLetters {
|
||||||
|
|
||||||
|
private static final Set<Path> A_TO_Z;
|
||||||
|
|
||||||
|
static {
|
||||||
|
var sortedSet = new TreeSet<Path>();
|
||||||
|
IntStream.rangeClosed('A', 'Z').mapToObj(i -> Path.of((char) i + ":\\")).forEach(sortedSet::add);
|
||||||
|
A_TO_Z = Collections.unmodifiableSet(sortedSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public WindowsDriveLetters() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Path> getAll() {
|
||||||
|
return A_TO_Z;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Path> getOccupied() {
|
||||||
|
if (!SystemUtils.IS_OS_WINDOWS) {
|
||||||
|
return Set.of();
|
||||||
|
} else {
|
||||||
|
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
|
||||||
|
return StreamSupport.stream(rootDirs.spliterator(), false).collect(Collectors.toUnmodifiableSet());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Path> getAvailable() {
|
||||||
|
return Sets.difference(getAll(), getOccupied());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips A and B and only returns them if all others are occupied.
|
||||||
|
*
|
||||||
|
* @return an Optional containing either the letter of a free drive letter or empty, if none is available
|
||||||
|
*/
|
||||||
|
public Optional<Path> getFirstDesiredAvailable() {
|
||||||
|
var availableDriveLetters = getAvailable();
|
||||||
|
var optString = availableDriveLetters.stream().filter(this::notAOrB).findFirst();
|
||||||
|
return optString.or(() -> availableDriveLetters.stream().findFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean notAOrB(Path driveLetter) {
|
||||||
|
return !(Path.of("A:\\").equals(driveLetter) || Path.of("B:\\").equals(driveLetter));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package org.cryptomator.common.mountpoint;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.vaults.Volume;
|
|
||||||
import org.cryptomator.common.vaults.WindowsDriveLetters;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class AvailableDriveLetterChooser implements MountPointChooser {
|
|
||||||
|
|
||||||
private final WindowsDriveLetters windowsDriveLetters;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public AvailableDriveLetterChooser(WindowsDriveLetters windowsDriveLetters) {
|
|
||||||
this.windowsDriveLetters = windowsDriveLetters;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isApplicable(Volume caller) {
|
|
||||||
return SystemUtils.IS_OS_WINDOWS;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
|
||||||
return this.windowsDriveLetters.getDesiredAvailableDriveLetterPath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package org.cryptomator.common.mountpoint;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
|
||||||
import org.cryptomator.common.vaults.Volume;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.LinkOption;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class CustomDriveLetterChooser implements MountPointChooser {
|
|
||||||
|
|
||||||
private final VaultSettings vaultSettings;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public CustomDriveLetterChooser(VaultSettings vaultSettings) {
|
|
||||||
this.vaultSettings = vaultSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isApplicable(Volume caller) {
|
|
||||||
return SystemUtils.IS_OS_WINDOWS;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
|
||||||
return this.vaultSettings.getWinDriveLetter().map(letter -> letter.charAt(0) + ":\\").map(Paths::get);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean prepare(Volume caller, Path driveLetter) throws InvalidMountPointException {
|
|
||||||
if (!Files.notExists(driveLetter, LinkOption.NOFOLLOW_LINKS)) {
|
|
||||||
//Drive already exists OR can't be determined
|
|
||||||
throw new InvalidMountPointException(new FileAlreadyExistsException(driveLetter.toString()));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
package org.cryptomator.common.mountpoint;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
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.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 {
|
|
||||||
waitForMountpointRestoration(mountPoint);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//on Windows removing the mountpoint takes some time, so we poll for at most 3 seconds
|
|
||||||
private void waitForMountpointRestoration(Path mountPoint) throws FileAlreadyExistsException {
|
|
||||||
int attempts = 0;
|
|
||||||
while (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
|
|
||||||
attempts++;
|
|
||||||
if (attempts >= 10) {
|
|
||||||
throw new FileAlreadyExistsException("Timeout waiting for mountpoint cleanup for " + mountPoint + " .");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Thread.sleep(300);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new FileAlreadyExistsException("Interrupted before mountpoint " + mountPoint + " was cleared");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkIsDirectory(Path toCheck) throws InvalidMountPointException {
|
|
||||||
if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) {
|
|
||||||
throw new InvalidMountPointException(new NotDirectoryException(toCheck.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkIsEmpty(Path toCheck) throws InvalidMountPointException, IOException {
|
|
||||||
try (var dirStream = Files.list(toCheck)) {
|
|
||||||
if (dirStream.findFirst().isPresent()) {
|
|
||||||
throw new InvalidMountPointException(new DirectoryNotEmptyException(toCheck.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//visible for testing
|
|
||||||
Path getHideaway(Path mountPoint) {
|
|
||||||
return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package org.cryptomator.common.mountpoint;
|
|
||||||
|
|
||||||
public class InvalidMountPointException extends Exception {
|
|
||||||
|
|
||||||
public InvalidMountPointException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InvalidMountPointException(Throwable cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InvalidMountPointException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package org.cryptomator.common.mountpoint;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
|
||||||
import org.cryptomator.common.vaults.Volume;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class MacVolumeMountChooser implements MountPointChooser {
|
|
||||||
|
|
||||||
private static final Path VOLUME_PATH = Path.of("/Volumes");
|
|
||||||
|
|
||||||
private final VaultSettings vaultSettings;
|
|
||||||
private final MountPointHelper helper;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public MacVolumeMountChooser(VaultSettings vaultSettings, MountPointHelper helper) {
|
|
||||||
this.vaultSettings = vaultSettings;
|
|
||||||
this.helper = helper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isApplicable(Volume caller) {
|
|
||||||
return SystemUtils.IS_OS_MAC;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
|
||||||
return Optional.of(helper.chooseTemporaryMountPoint(vaultSettings, VOLUME_PATH));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean prepare(Volume caller, Path mountPoint) {
|
|
||||||
// https://github.com/osxfuse/osxfuse/issues/306#issuecomment-245114592:
|
|
||||||
// In order to allow non-admin users to mount FUSE volumes in `/Volumes`,
|
|
||||||
// starting with version 3.5.0, FUSE will create non-existent mount points automatically.
|
|
||||||
// Therefore we don't need to prepare anything.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
package org.cryptomator.common.mountpoint;
|
|
||||||
|
|
||||||
import dagger.multibindings.IntKey;
|
|
||||||
import org.cryptomator.common.vaults.Volume;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.SortedSet;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base interface for the Mountpoint-Choosing-Operation that results in the choice and
|
|
||||||
* preparation of a mountpoint or an exception otherwise.<br>
|
|
||||||
* <p>All <i>MountPointChoosers (MPCs)</i> need to implement this class and must be added to
|
|
||||||
* the pool of possible MPCs by the {@link MountPointChooserModule MountPointChooserModule.}
|
|
||||||
* The MountPointChooserModule will sort them according to their {@link IntKey IntKey priority.}
|
|
||||||
* The priority must be defined by the developer to reflect a useful execution order.<br>
|
|
||||||
* A specific priority <b>must not</b> be assigned to more than one MPC at a time;
|
|
||||||
* the result of having two MPCs with equal priority is undefined.
|
|
||||||
*
|
|
||||||
* <p>MPCs are executed by a {@link Volume} in descending order of their priority
|
|
||||||
* (higher priorities are tried first) to find and prepare a suitable mountpoint for the volume.
|
|
||||||
* The volume has access to a {@link SortedSet} of MPCs in this specific order,
|
|
||||||
* that is provided by the Module. The Set contains all available Choosers, even if they
|
|
||||||
* are not {@link #isApplicable(Volume) applicable} for the Vault/Volume. The Volume must
|
|
||||||
* check whether a MPC is applicable by invoking {@code #isApplicable(Volume)} on it
|
|
||||||
* <i>before</i> calling {@code #chooseMountPoint(Volume)}.
|
|
||||||
*
|
|
||||||
* <p>At execution of a MPC {@link #chooseMountPoint(Volume)} is called to choose a mountpoint
|
|
||||||
* according to the MPC's <i>strategy.</i> The <i>strategy</i> can involve reading configs,
|
|
||||||
* searching the filesystem, processing user-input or similar operations.
|
|
||||||
* If {@code #chooseMountPoint(Volume)} returns a non-null path (everything but
|
|
||||||
* {@linkplain Optional#empty()}) the MPC's {@link #prepare(Volume, Path)} method is called and the
|
|
||||||
* MountPoint is verified and/or prepared. In this case <i>no other MPC's will be called for
|
|
||||||
* this volume, even if {@code #prepare(Volume, Path)} fails.</i>
|
|
||||||
*
|
|
||||||
* <p>If {@code #chooseMountPoint(Volume)} yields no result, the next MPC is executed
|
|
||||||
* <i>without</i> first calling the {@code #prepare(Volume, Path)} method of the current MPC.
|
|
||||||
* This is repeated until<br>
|
|
||||||
* <ul>
|
|
||||||
* <li><b>either</b> a mountpoint is returned by {@code #chooseMountPoint(Volume)}
|
|
||||||
* and {@code #prepare(Volume, Path)} succeeds or fails, ending the entire operation</li>
|
|
||||||
* <li><b>or</b> no MPC remains and an {@link InvalidMountPointException} is thrown.</li>
|
|
||||||
* </ul>
|
|
||||||
* If the {@code #prepare(Volume, Path)} method of a MPC fails, the entire
|
|
||||||
* Mountpoint-Choosing-Operation is aborted and the method should do all necessary cleanup
|
|
||||||
* before throwing the exception.
|
|
||||||
* If the preparation succeeds {@link #cleanup(Volume, Path)} can be used after unmount to do any
|
|
||||||
* remaining cleanup.
|
|
||||||
*/
|
|
||||||
public interface MountPointChooser {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the {@link Volume} to determine whether this MountPointChooser is
|
|
||||||
* applicable for mounting the Vault/Volume, especially with regard to the
|
|
||||||
* current system configuration and particularities of the Volume type.
|
|
||||||
*
|
|
||||||
* <p>Developers should override this method to check for system configurations
|
|
||||||
* that are unsuitable for this MPC.
|
|
||||||
*
|
|
||||||
* @param caller The Volume that is calling the method to determine applicability of the MPC
|
|
||||||
* @return a boolean flag; true if applicable, else false.
|
|
||||||
* @see #chooseMountPoint(Volume)
|
|
||||||
*/
|
|
||||||
boolean isApplicable(Volume caller);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by a {@link Volume} to choose a mountpoint according to the
|
|
||||||
* MountPointChoosers strategy.
|
|
||||||
*
|
|
||||||
* <p>This method must only be called for MPCs that were deemed
|
|
||||||
* {@link #isApplicable(Volume) applicable} by the {@link Volume Volume.}
|
|
||||||
* Developers should override this method to find or extract a mountpoint for
|
|
||||||
* the volume <b>without</b> preparing it. Preparation should be done by
|
|
||||||
* {@link #prepare(Volume, Path)} instead.
|
|
||||||
* Exceptions in this method should be handled gracefully and result in returning
|
|
||||||
* {@link Optional#empty()} instead of throwing an exception.
|
|
||||||
*
|
|
||||||
* @param caller The Volume that is calling the method to choose a mountpoint
|
|
||||||
* @return the chosen path or {@link Optional#empty()} if an exception occurred
|
|
||||||
* or no mountpoint could be found.
|
|
||||||
* @see #isApplicable(Volume)
|
|
||||||
* @see #prepare(Volume, Path)
|
|
||||||
*/
|
|
||||||
Optional<Path> chooseMountPoint(Volume caller);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by a {@link Volume} to prepare and/or verify the chosen mountpoint.<br>
|
|
||||||
* This method is only called if the {@link #chooseMountPoint(Volume)} method
|
|
||||||
* of the same MountPointChooser returned a path.
|
|
||||||
*
|
|
||||||
* <p>Developers should override this method to prepare the mountpoint for
|
|
||||||
* the volume and check for any obstacles that could hinder the mount operation.
|
|
||||||
* The mountpoint is deemed "prepared" if it can be used to mount a volume
|
|
||||||
* without any further filesystem actions or user interaction. If this is not possible,
|
|
||||||
* this method should fail. In other words: This method should not return without
|
|
||||||
* either failing or finalizing the preparation of the mountpoint.
|
|
||||||
* Generally speaking exceptions should be wrapped as
|
|
||||||
* {@link InvalidMountPointException} to allow efficient handling by the caller.
|
|
||||||
*
|
|
||||||
* <p>Often the preparation of a mountpoint involves creating files or others
|
|
||||||
* actions that require cleaning up after the volume is unmounted.
|
|
||||||
* In this case developers should override the {@link #cleanup(Volume, Path)}
|
|
||||||
* method and return {@code true} to the volume to indicate that the
|
|
||||||
* {@code #cleanup} method of this MPC should be called after unmount.
|
|
||||||
*
|
|
||||||
* <p><b>Please note:</b> If this method fails the entire
|
|
||||||
* Mountpoint-Choosing-Operation is aborted without calling
|
|
||||||
* {@link #cleanup(Volume, Path)} or any other MPCs. Therefore this method should
|
|
||||||
* do all necessary cleanup before throwing the exception.
|
|
||||||
*
|
|
||||||
* @param caller The Volume that is calling the method to prepare a mountpoint
|
|
||||||
* @param mountPoint the mountpoint chosen by {@link #chooseMountPoint(Volume)}
|
|
||||||
* @return a boolean flag; true if cleanup is needed, false otherwise
|
|
||||||
* @throws InvalidMountPointException if the preparation fails
|
|
||||||
* @see #chooseMountPoint(Volume)
|
|
||||||
* @see #cleanup(Volume, Path)
|
|
||||||
*/
|
|
||||||
default boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
|
||||||
return false; //NO-OP
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by a {@link Volume} to do any cleanup needed after unmount.
|
|
||||||
*
|
|
||||||
* <p>This method is only called if the {@link #prepare(Volume, Path)} method
|
|
||||||
* of the same MountPointChooser returned {@code true}. Typically developers want to
|
|
||||||
* delete any files created prior to mount or do similar tasks.<br>
|
|
||||||
* Exceptions in this method should be handled gracefully.
|
|
||||||
*
|
|
||||||
* @param caller The Volume that is calling the method to cleanup the prepared mountpoint
|
|
||||||
* @param mountPoint the mountpoint that was prepared by {@link #prepare(Volume, Path)}
|
|
||||||
* @see #prepare(Volume, Path)
|
|
||||||
*/
|
|
||||||
default void cleanup(Volume caller, Path mountPoint) {
|
|
||||||
//NO-OP
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package org.cryptomator.common.mountpoint;
|
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
|
||||||
import dagger.Binds;
|
|
||||||
import dagger.Module;
|
|
||||||
import dagger.Provides;
|
|
||||||
import dagger.multibindings.IntKey;
|
|
||||||
import dagger.multibindings.IntoMap;
|
|
||||||
import org.cryptomator.common.vaults.PerVault;
|
|
||||||
|
|
||||||
import javax.inject.Named;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.SortedMap;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dagger-Module for {@link MountPointChooser MountPointChoosers.}<br>
|
|
||||||
* See there for additional information.
|
|
||||||
*
|
|
||||||
* @see MountPointChooser
|
|
||||||
*/
|
|
||||||
@Module
|
|
||||||
public abstract class MountPointChooserModule {
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
@IntoMap
|
|
||||||
@IntKey(1000)
|
|
||||||
@PerVault
|
|
||||||
public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser);
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
@IntoMap
|
|
||||||
@IntKey(900)
|
|
||||||
@PerVault
|
|
||||||
public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser);
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
@IntoMap
|
|
||||||
@IntKey(800)
|
|
||||||
@PerVault
|
|
||||||
public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
@IntoMap
|
|
||||||
@IntKey(101)
|
|
||||||
@PerVault
|
|
||||||
public abstract MountPointChooser bindMacVolumeMountChooser(MacVolumeMountChooser chooser);
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
@IntoMap
|
|
||||||
@IntKey(100)
|
|
||||||
@PerVault
|
|
||||||
public abstract MountPointChooser bindTemporaryMountPointChooser(TemporaryMountPointChooser chooser);
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@PerVault
|
|
||||||
@Named("orderedMountPointChoosers")
|
|
||||||
public static Iterable<MountPointChooser> provideOrderedMountPointChoosers(Map<Integer, MountPointChooser> choosers) {
|
|
||||||
SortedMap<Integer, MountPointChooser> sortedChoosers = new TreeMap<>(Comparator.reverseOrder());
|
|
||||||
sortedChoosers.putAll(choosers);
|
|
||||||
return Iterables.unmodifiableIterable(sortedChoosers.values());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package org.cryptomator.common.mountpoint;
|
|
||||||
|
|
||||||
import org.cryptomator.common.Environment;
|
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.DirectoryNotEmptyException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.LinkOption;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class MountPointHelper {
|
|
||||||
|
|
||||||
public static Logger LOG = LoggerFactory.getLogger(MountPointHelper.class);
|
|
||||||
private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
|
|
||||||
|
|
||||||
private final Optional<Path> tmpMountPointDir;
|
|
||||||
private volatile boolean unmountDebrisCleared = false;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public MountPointHelper(Environment env) {
|
|
||||||
this.tmpMountPointDir = env.getMountPointsDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path chooseTemporaryMountPoint(VaultSettings vaultSettings, Path parentDir) {
|
|
||||||
String basename = vaultSettings.mountName().get();
|
|
||||||
//regular
|
|
||||||
Path mountPoint = parentDir.resolve(basename);
|
|
||||||
if (Files.notExists(mountPoint)) {
|
|
||||||
return mountPoint;
|
|
||||||
}
|
|
||||||
//with id
|
|
||||||
mountPoint = parentDir.resolve(basename + " (" + vaultSettings.getId() + ")");
|
|
||||||
if (Files.notExists(mountPoint)) {
|
|
||||||
return mountPoint;
|
|
||||||
}
|
|
||||||
//with id and count
|
|
||||||
for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
|
|
||||||
mountPoint = parentDir.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
|
|
||||||
if (Files.notExists(mountPoint)) {
|
|
||||||
return mountPoint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parentDir, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void clearIrregularUnmountDebrisIfNeeded() {
|
|
||||||
if (unmountDebrisCleared || tmpMountPointDir.isEmpty()) {
|
|
||||||
return; // nothing to do
|
|
||||||
}
|
|
||||||
if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) {
|
|
||||||
clearIrregularUnmountDebris(tmpMountPointDir.get());
|
|
||||||
}
|
|
||||||
unmountDebrisCleared = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {
|
|
||||||
IOException cleanupFailed = new IOException("Cleanup failed");
|
|
||||||
|
|
||||||
try (var ds = Files.newDirectoryStream(dirContainingMountPoints)) {
|
|
||||||
LOG.debug("Performing cleanup of mountpoint dir {}.", dirContainingMountPoints);
|
|
||||||
for (Path p : ds) {
|
|
||||||
try {
|
|
||||||
var attr = Files.readAttributes(p, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
|
|
||||||
if (attr.isOther() && attr.isDirectory()) { // yes, this is possible with windows junction points -.-
|
|
||||||
Files.delete(p);
|
|
||||||
} else if (attr.isDirectory()) {
|
|
||||||
deleteEmptyDir(p);
|
|
||||||
} else if (attr.isSymbolicLink()) {
|
|
||||||
deleteDeadLink(p);
|
|
||||||
} else {
|
|
||||||
LOG.debug("Found non-directory element in mountpoint dir: {}", p);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
cleanupFailed.addSuppressed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanupFailed.getSuppressed().length > 0) {
|
|
||||||
throw cleanupFailed;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e);
|
|
||||||
} finally {
|
|
||||||
unmountDebrisCleared = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteEmptyDir(Path dir) throws IOException {
|
|
||||||
assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
|
|
||||||
try {
|
|
||||||
ensureIsEmpty(dir);
|
|
||||||
Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents)
|
|
||||||
} catch (DirectoryNotEmptyException e) {
|
|
||||||
LOG.info("Found non-empty directory in mountpoint dir: {}", dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteDeadLink(Path symlink) throws IOException {
|
|
||||||
assert Files.isSymbolicLink(symlink);
|
|
||||||
if (Files.notExists(symlink)) { // following link: target does not exist
|
|
||||||
Files.delete(symlink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureIsEmpty(Path dir) throws IOException {
|
|
||||||
try (var ds = Files.newDirectoryStream(dir)) {
|
|
||||||
if (ds.iterator().hasNext()){
|
|
||||||
throw new DirectoryNotEmptyException(dir.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package org.cryptomator.common.mountpoint;
|
|
||||||
|
|
||||||
import org.cryptomator.common.Environment;
|
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
|
||||||
import org.cryptomator.common.vaults.Volume;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class TemporaryMountPointChooser implements MountPointChooser {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
|
|
||||||
|
|
||||||
private final VaultSettings vaultSettings;
|
|
||||||
private final Environment environment;
|
|
||||||
private final MountPointHelper helper;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, MountPointHelper helper) {
|
|
||||||
this.vaultSettings = vaultSettings;
|
|
||||||
this.environment = environment;
|
|
||||||
this.helper = helper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isApplicable(Volume caller) {
|
|
||||||
if (this.environment.getMountPointsDir().isEmpty()) {
|
|
||||||
LOG.warn("\"cryptomator.mountPointsDir\" is not set to a valid path!");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
|
||||||
assert environment.getMountPointsDir().isPresent();
|
|
||||||
//clean leftovers of not-regularly unmounted vaults
|
|
||||||
//see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061
|
|
||||||
helper.clearIrregularUnmountDebrisIfNeeded();
|
|
||||||
return this.environment.getMountPointsDir().map(dir -> this.helper.chooseTemporaryMountPoint(this.vaultSettings, dir));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
|
||||||
try {
|
|
||||||
switch (caller.getMountPointRequirement()) {
|
|
||||||
case PARENT_NO_MOUNT_POINT -> {
|
|
||||||
Files.createDirectories(mountPoint.getParent());
|
|
||||||
LOG.debug("Successfully created folder for mount point: {}", mountPoint);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
case EMPTY_MOUNT_POINT -> {
|
|
||||||
Files.createDirectories(mountPoint);
|
|
||||||
LOG.debug("Successfully created mount point: {}", mountPoint);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case NONE -> {
|
|
||||||
//Requirement "NONE" doesn't make any sense here.
|
|
||||||
//No need to prepare/verify a Mountpoint without requiring one...
|
|
||||||
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
|
|
||||||
}
|
|
||||||
default -> {
|
|
||||||
//Currently the case for "UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT"
|
|
||||||
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException exception) {
|
|
||||||
throw new InvalidMountPointException("IOException while preparing mountpoint", exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void cleanup(Volume caller, Path mountPoint) {
|
|
||||||
try {
|
|
||||||
Files.delete(mountPoint);
|
|
||||||
LOG.debug("Successfully deleted mount point: {}", mountPoint);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warn("Could not delete mount point: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -76,11 +76,6 @@ public class DeviceKey {
|
|||||||
|
|
||||||
private P384KeyPair createAndStoreNewKeyPair(char[] passphrase, Path p12File) throws IOException {
|
private P384KeyPair createAndStoreNewKeyPair(char[] passphrase, Path p12File) throws IOException {
|
||||||
var keyPair = P384KeyPair.generate();
|
var keyPair = P384KeyPair.generate();
|
||||||
var tmpFile = p12File.resolveSibling(p12File.getFileName().toString() + ".tmp");
|
|
||||||
if(Files.exists(tmpFile)) {
|
|
||||||
LOG.debug("Leftover from devicekey creation detected. Deleting {}", tmpFile);
|
|
||||||
Files.delete(tmpFile);
|
|
||||||
}
|
|
||||||
LOG.debug("Store new device key to {}", p12File);
|
LOG.debug("Store new device key to {}", p12File);
|
||||||
keyPair.store(p12File, passphrase);
|
keyPair.store(p12File, passphrase);
|
||||||
return keyPair;
|
return keyPair;
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ public class Settings {
|
|||||||
public static final boolean DEFAULT_USE_KEYCHAIN = true;
|
public static final boolean DEFAULT_USE_KEYCHAIN = true;
|
||||||
public static final int DEFAULT_PORT = 42427;
|
public static final int DEFAULT_PORT = 42427;
|
||||||
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
|
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
|
||||||
public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV;
|
|
||||||
public static final boolean DEFAULT_DEBUG_MODE = false;
|
public static final boolean DEFAULT_DEBUG_MODE = false;
|
||||||
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = VolumeImpl.FUSE;
|
|
||||||
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
|
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
|
||||||
@Deprecated // to be changed to "whatever is available" eventually
|
@Deprecated // to be changed to "whatever is available" eventually
|
||||||
public static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : "org.cryptomator.linux.keychain.SecretServiceKeychainAccess";
|
public static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : "org.cryptomator.linux.keychain.SecretServiceKeychainAccess";
|
||||||
@@ -57,9 +55,7 @@ public class Settings {
|
|||||||
private final BooleanProperty useKeychain = new SimpleBooleanProperty(DEFAULT_USE_KEYCHAIN);
|
private final BooleanProperty useKeychain = new SimpleBooleanProperty(DEFAULT_USE_KEYCHAIN);
|
||||||
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
|
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
|
||||||
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
|
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
|
||||||
private final ObjectProperty<WebDavUrlScheme> preferredGvfsScheme = new SimpleObjectProperty<>(DEFAULT_GVFS_SCHEME);
|
|
||||||
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
|
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
|
||||||
private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
|
|
||||||
private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
|
private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
|
||||||
private final ObjectProperty<String> keychainProvider = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_PROVIDER);
|
private final ObjectProperty<String> keychainProvider = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_PROVIDER);
|
||||||
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
|
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
|
||||||
@@ -74,6 +70,9 @@ public class Settings {
|
|||||||
private final StringProperty language = new SimpleStringProperty(DEFAULT_LANGUAGE);
|
private final StringProperty language = new SimpleStringProperty(DEFAULT_LANGUAGE);
|
||||||
|
|
||||||
|
|
||||||
|
private final StringProperty mountService = new SimpleStringProperty();
|
||||||
|
|
||||||
|
|
||||||
private Consumer<Settings> saveCmd;
|
private Consumer<Settings> saveCmd;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,9 +89,7 @@ public class Settings {
|
|||||||
useKeychain.addListener(this::somethingChanged);
|
useKeychain.addListener(this::somethingChanged);
|
||||||
port.addListener(this::somethingChanged);
|
port.addListener(this::somethingChanged);
|
||||||
numTrayNotifications.addListener(this::somethingChanged);
|
numTrayNotifications.addListener(this::somethingChanged);
|
||||||
preferredGvfsScheme.addListener(this::somethingChanged);
|
|
||||||
debugMode.addListener(this::somethingChanged);
|
debugMode.addListener(this::somethingChanged);
|
||||||
preferredVolumeImpl.addListener(this::somethingChanged);
|
|
||||||
theme.addListener(this::somethingChanged);
|
theme.addListener(this::somethingChanged);
|
||||||
keychainProvider.addListener(this::somethingChanged);
|
keychainProvider.addListener(this::somethingChanged);
|
||||||
userInterfaceOrientation.addListener(this::somethingChanged);
|
userInterfaceOrientation.addListener(this::somethingChanged);
|
||||||
@@ -105,6 +102,7 @@ public class Settings {
|
|||||||
windowHeight.addListener(this::somethingChanged);
|
windowHeight.addListener(this::somethingChanged);
|
||||||
displayConfiguration.addListener(this::somethingChanged);
|
displayConfiguration.addListener(this::somethingChanged);
|
||||||
language.addListener(this::somethingChanged);
|
language.addListener(this::somethingChanged);
|
||||||
|
mountService.addListener(this::somethingChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSaveCmd(Consumer<Settings> saveCmd) {
|
void setSaveCmd(Consumer<Settings> saveCmd) {
|
||||||
@@ -153,16 +151,12 @@ public class Settings {
|
|||||||
return numTrayNotifications;
|
return numTrayNotifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectProperty<WebDavUrlScheme> preferredGvfsScheme() {
|
|
||||||
return preferredGvfsScheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BooleanProperty debugMode() {
|
public BooleanProperty debugMode() {
|
||||||
return debugMode;
|
return debugMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectProperty<VolumeImpl> preferredVolumeImpl() {
|
public StringProperty mountService() {
|
||||||
return preferredVolumeImpl;
|
return mountService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectProperty<UiTheme> theme() {
|
public ObjectProperty<UiTheme> theme() {
|
||||||
@@ -210,4 +204,5 @@ public class Settings {
|
|||||||
public StringProperty languageProperty() {
|
public StringProperty languageProperty() {
|
||||||
return language;
|
return language;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
out.name("autoCloseVaults").value(value.autoCloseVaults().get());
|
out.name("autoCloseVaults").value(value.autoCloseVaults().get());
|
||||||
out.name("port").value(value.port().get());
|
out.name("port").value(value.port().get());
|
||||||
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
|
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
|
||||||
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get().name());
|
|
||||||
out.name("debugMode").value(value.debugMode().get());
|
out.name("debugMode").value(value.debugMode().get());
|
||||||
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
|
|
||||||
out.name("theme").value(value.theme().get().name());
|
out.name("theme").value(value.theme().get().name());
|
||||||
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
|
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
|
||||||
out.name("keychainProvider").value(value.keychainProvider().get());
|
out.name("keychainProvider").value(value.keychainProvider().get());
|
||||||
@@ -60,7 +58,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
out.name("windowHeight").value((value.windowHeightProperty().get()));
|
out.name("windowHeight").value((value.windowHeightProperty().get()));
|
||||||
out.name("displayConfiguration").value((value.displayConfigurationProperty().get()));
|
out.name("displayConfiguration").value((value.displayConfigurationProperty().get()));
|
||||||
out.name("language").value((value.languageProperty().get()));
|
out.name("language").value((value.languageProperty().get()));
|
||||||
|
out.name("mountService").value(value.mountService().get());
|
||||||
out.endObject();
|
out.endObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,9 +85,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
case "autoCloseVaults" -> settings.autoCloseVaults().set(in.nextBoolean());
|
case "autoCloseVaults" -> settings.autoCloseVaults().set(in.nextBoolean());
|
||||||
case "port" -> settings.port().set(in.nextInt());
|
case "port" -> settings.port().set(in.nextInt());
|
||||||
case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
|
case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
|
||||||
case "preferredGvfsScheme" -> settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
|
|
||||||
case "debugMode" -> settings.debugMode().set(in.nextBoolean());
|
case "debugMode" -> settings.debugMode().set(in.nextBoolean());
|
||||||
case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
|
|
||||||
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
|
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
|
||||||
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
|
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
|
||||||
case "keychainProvider" -> settings.keychainProvider().set(in.nextString());
|
case "keychainProvider" -> settings.keychainProvider().set(in.nextString());
|
||||||
@@ -103,6 +99,12 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
case "windowHeight" -> settings.windowHeightProperty().set(in.nextInt());
|
case "windowHeight" -> settings.windowHeightProperty().set(in.nextInt());
|
||||||
case "displayConfiguration" -> settings.displayConfigurationProperty().set(in.nextString());
|
case "displayConfiguration" -> settings.displayConfigurationProperty().set(in.nextString());
|
||||||
case "language" -> settings.languageProperty().set(in.nextString());
|
case "language" -> settings.languageProperty().set(in.nextString());
|
||||||
|
case "mountService" -> {
|
||||||
|
var token = in.peek();
|
||||||
|
if (JsonToken.STRING == token) {
|
||||||
|
settings.mountService().set(in.nextString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default -> {
|
default -> {
|
||||||
LOG.warn("Unsupported vault setting found in JSON: {}", name);
|
LOG.warn("Unsupported vault setting found in JSON: {}", name);
|
||||||
@@ -115,24 +117,6 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VolumeImpl parsePreferredVolumeImplName(String nioAdapterName) {
|
|
||||||
try {
|
|
||||||
return VolumeImpl.valueOf(nioAdapterName.toUpperCase());
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
LOG.warn("Invalid volume type {}. Defaulting to {}.", nioAdapterName, Settings.DEFAULT_PREFERRED_VOLUME_IMPL);
|
|
||||||
return Settings.DEFAULT_PREFERRED_VOLUME_IMPL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private WebDavUrlScheme parseWebDavUrlSchemePrefix(String webDavUrlSchemeName) {
|
|
||||||
try {
|
|
||||||
return WebDavUrlScheme.valueOf(webDavUrlSchemeName.toUpperCase());
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
LOG.warn("Invalid WebDAV url scheme {}. Defaulting to {}.", webDavUrlSchemeName, Settings.DEFAULT_GVFS_SCHEME);
|
|
||||||
return Settings.DEFAULT_GVFS_SCHEME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private UiTheme parseUiTheme(String uiThemeName) {
|
private UiTheme parseUiTheme(String uiThemeName) {
|
||||||
try {
|
try {
|
||||||
return UiTheme.valueOf(uiThemeName.toUpperCase());
|
return UiTheme.valueOf(uiThemeName.toUpperCase());
|
||||||
|
|||||||
@@ -6,12 +6,10 @@
|
|||||||
package org.cryptomator.common.settings;
|
package org.cryptomator.common.settings;
|
||||||
|
|
||||||
import com.google.common.base.CharMatcher;
|
import com.google.common.base.CharMatcher;
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
|
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.StringBinding;
|
|
||||||
import javafx.beans.binding.StringExpression;
|
import javafx.beans.binding.StringExpression;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
@@ -21,10 +19,8 @@ import javafx.beans.property.SimpleIntegerProperty;
|
|||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,7 +30,6 @@ public class VaultSettings {
|
|||||||
|
|
||||||
public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false;
|
public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false;
|
||||||
public static final boolean DEFAULT_REVEAL_AFTER_MOUNT = true;
|
public static final boolean DEFAULT_REVEAL_AFTER_MOUNT = true;
|
||||||
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
|
|
||||||
public static final boolean DEFAULT_USES_READONLY_MODE = false;
|
public static final boolean DEFAULT_USES_READONLY_MODE = false;
|
||||||
public static final String DEFAULT_MOUNT_FLAGS = "";
|
public static final String DEFAULT_MOUNT_FLAGS = "";
|
||||||
public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
|
public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
|
||||||
@@ -47,11 +42,8 @@ public class VaultSettings {
|
|||||||
private final String id;
|
private final String id;
|
||||||
private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
|
private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
|
||||||
private final StringProperty displayName = new SimpleStringProperty();
|
private final StringProperty displayName = new SimpleStringProperty();
|
||||||
private final StringProperty winDriveLetter = new SimpleStringProperty();
|
|
||||||
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
|
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
|
||||||
private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REVEAL_AFTER_MOUNT);
|
private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REVEAL_AFTER_MOUNT);
|
||||||
private final BooleanProperty useCustomMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
|
|
||||||
private final StringProperty customMountPath = new SimpleStringProperty();
|
|
||||||
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
|
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
|
||||||
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
|
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
|
||||||
private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
|
private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
|
||||||
@@ -59,14 +51,23 @@ public class VaultSettings {
|
|||||||
private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE);
|
private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE);
|
||||||
private final IntegerProperty autoLockIdleSeconds = new SimpleIntegerProperty(DEFAULT_AUTOLOCK_IDLE_SECONDS);
|
private final IntegerProperty autoLockIdleSeconds = new SimpleIntegerProperty(DEFAULT_AUTOLOCK_IDLE_SECONDS);
|
||||||
private final StringExpression mountName;
|
private final StringExpression mountName;
|
||||||
|
private final ObjectProperty<Path> mountPoint = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
public VaultSettings(String id) {
|
public VaultSettings(String id) {
|
||||||
this.id = Objects.requireNonNull(id);
|
this.id = Objects.requireNonNull(id);
|
||||||
this.mountName = StringExpression.stringExpression(displayName.map(VaultSettings::normalizeDisplayName).orElse(""));
|
this.mountName = StringExpression.stringExpression(Bindings.createStringBinding(() -> {
|
||||||
|
final String name;
|
||||||
|
if (displayName.isEmpty().get()) {
|
||||||
|
name = path.get().getFileName().toString();
|
||||||
|
} else {
|
||||||
|
name = displayName.get();
|
||||||
|
}
|
||||||
|
return normalizeDisplayName(name);
|
||||||
|
}, displayName, path));
|
||||||
}
|
}
|
||||||
|
|
||||||
Observable[] observables() {
|
Observable[] observables() {
|
||||||
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock, autoLockWhenIdle, autoLockIdleSeconds};
|
return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static VaultSettings withRandomId() {
|
public static VaultSettings withRandomId() {
|
||||||
@@ -110,18 +111,6 @@ public class VaultSettings {
|
|||||||
return mountName;
|
return mountName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty winDriveLetter() {
|
|
||||||
return winDriveLetter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<String> getWinDriveLetter() {
|
|
||||||
String current = this.winDriveLetter.get();
|
|
||||||
if (!Strings.isNullOrEmpty(current)) {
|
|
||||||
return Optional.of(current);
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public BooleanProperty unlockAfterStartup() {
|
public BooleanProperty unlockAfterStartup() {
|
||||||
return unlockAfterStartup;
|
return unlockAfterStartup;
|
||||||
}
|
}
|
||||||
@@ -130,20 +119,12 @@ public class VaultSettings {
|
|||||||
return revealAfterMount;
|
return revealAfterMount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanProperty useCustomMountPath() {
|
public Path getMountPoint() {
|
||||||
return useCustomMountPath;
|
return mountPoint.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty customMountPath() {
|
public ObjectProperty<Path> mountPoint() {
|
||||||
return customMountPath;
|
return mountPoint;
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<String> getCustomMountPath() {
|
|
||||||
if (useCustomMountPath.get()) {
|
|
||||||
return Optional.ofNullable(Strings.emptyToNull(customMountPath.get()));
|
|
||||||
} else {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanProperty usesReadOnlyMode() {
|
public BooleanProperty usesReadOnlyMode() {
|
||||||
@@ -189,5 +170,4 @@ public class VaultSettings {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,14 @@
|
|||||||
package org.cryptomator.common.settings;
|
package org.cryptomator.common.settings;
|
||||||
|
|
||||||
import com.google.gson.stream.JsonReader;
|
import com.google.gson.stream.JsonReader;
|
||||||
|
import com.google.gson.stream.JsonToken;
|
||||||
import com.google.gson.stream.JsonWriter;
|
import com.google.gson.stream.JsonWriter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.InvalidPathException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
class VaultSettingsJsonAdapter {
|
class VaultSettingsJsonAdapter {
|
||||||
@@ -22,11 +25,10 @@ class VaultSettingsJsonAdapter {
|
|||||||
out.name("id").value(value.getId());
|
out.name("id").value(value.getId());
|
||||||
out.name("path").value(value.path().get().toString());
|
out.name("path").value(value.path().get().toString());
|
||||||
out.name("displayName").value(value.displayName().get());
|
out.name("displayName").value(value.displayName().get());
|
||||||
out.name("winDriveLetter").value(value.winDriveLetter().get());
|
|
||||||
out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
|
out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
|
||||||
out.name("revealAfterMount").value(value.revealAfterMount().get());
|
out.name("revealAfterMount").value(value.revealAfterMount().get());
|
||||||
out.name("useCustomMountPath").value(value.useCustomMountPath().get());
|
var mountPoint = value.mountPoint().get();
|
||||||
out.name("customMountPath").value(value.customMountPath().get());
|
out.name("mountPoint").value(mountPoint != null ? mountPoint.toAbsolutePath().toString() : null);
|
||||||
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
|
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
|
||||||
out.name("mountFlags").value(value.mountFlags().get());
|
out.name("mountFlags").value(value.mountFlags().get());
|
||||||
out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get());
|
out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get());
|
||||||
@@ -36,18 +38,18 @@ class VaultSettingsJsonAdapter {
|
|||||||
out.endObject();
|
out.endObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: usesCustomMountPath, customMountPath and winDriveLetter removed
|
||||||
|
// -> migration required
|
||||||
public VaultSettings read(JsonReader in) throws IOException {
|
public VaultSettings read(JsonReader in) throws IOException {
|
||||||
String id = null;
|
String id = null;
|
||||||
String path = null;
|
String path = null;
|
||||||
String mountName = null; //see https://github.com/cryptomator/cryptomator/pull/1318
|
String mountName = null; //see https://github.com/cryptomator/cryptomator/pull/1318
|
||||||
String displayName = null;
|
String displayName = null;
|
||||||
String customMountPath = null;
|
|
||||||
String winDriveLetter = null;
|
|
||||||
boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
|
boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
|
||||||
boolean revealAfterMount = VaultSettings.DEFAULT_REVEAL_AFTER_MOUNT;
|
boolean revealAfterMount = VaultSettings.DEFAULT_REVEAL_AFTER_MOUNT;
|
||||||
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
|
|
||||||
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
|
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
|
||||||
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
|
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
|
||||||
|
Path mountPoint = null;
|
||||||
int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
|
int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
|
||||||
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
|
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
|
||||||
boolean autoLockWhenIdle = VaultSettings.DEFAULT_AUTOLOCK_WHEN_IDLE;
|
boolean autoLockWhenIdle = VaultSettings.DEFAULT_AUTOLOCK_WHEN_IDLE;
|
||||||
@@ -61,13 +63,17 @@ class VaultSettingsJsonAdapter {
|
|||||||
case "path" -> path = in.nextString();
|
case "path" -> path = in.nextString();
|
||||||
case "mountName" -> mountName = in.nextString(); //see https://github.com/cryptomator/cryptomator/pull/1318
|
case "mountName" -> mountName = in.nextString(); //see https://github.com/cryptomator/cryptomator/pull/1318
|
||||||
case "displayName" -> displayName = in.nextString();
|
case "displayName" -> displayName = in.nextString();
|
||||||
case "winDriveLetter" -> winDriveLetter = in.nextString();
|
|
||||||
case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean();
|
case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean();
|
||||||
case "revealAfterMount" -> revealAfterMount = in.nextBoolean();
|
case "revealAfterMount" -> revealAfterMount = in.nextBoolean();
|
||||||
case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
|
|
||||||
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
|
|
||||||
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
|
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
|
||||||
case "mountFlags" -> mountFlags = in.nextString();
|
case "mountFlags" -> mountFlags = in.nextString();
|
||||||
|
case "mountPoint" -> {
|
||||||
|
if (JsonToken.NULL == in.peek()) {
|
||||||
|
in.nextNull();
|
||||||
|
} else {
|
||||||
|
mountPoint = parseMountPoint(in.nextString());
|
||||||
|
}
|
||||||
|
}
|
||||||
case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
|
case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
|
||||||
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
|
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
|
||||||
case "autoLockWhenIdle" -> autoLockWhenIdle = in.nextBoolean();
|
case "autoLockWhenIdle" -> autoLockWhenIdle = in.nextBoolean();
|
||||||
@@ -87,20 +93,27 @@ class VaultSettingsJsonAdapter {
|
|||||||
vaultSettings.displayName().set(mountName);
|
vaultSettings.displayName().set(mountName);
|
||||||
}
|
}
|
||||||
vaultSettings.path().set(Paths.get(path));
|
vaultSettings.path().set(Paths.get(path));
|
||||||
vaultSettings.winDriveLetter().set(winDriveLetter);
|
|
||||||
vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
|
vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
|
||||||
vaultSettings.revealAfterMount().set(revealAfterMount);
|
vaultSettings.revealAfterMount().set(revealAfterMount);
|
||||||
vaultSettings.useCustomMountPath().set(useCustomMountPath);
|
|
||||||
vaultSettings.customMountPath().set(customMountPath);
|
|
||||||
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
|
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
|
||||||
vaultSettings.mountFlags().set(mountFlags);
|
vaultSettings.mountFlags().set(mountFlags);
|
||||||
vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
|
vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
|
||||||
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
|
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
|
||||||
vaultSettings.autoLockWhenIdle().set(autoLockWhenIdle);
|
vaultSettings.autoLockWhenIdle().set(autoLockWhenIdle);
|
||||||
vaultSettings.autoLockIdleSeconds().set(autoLockIdleSeconds);
|
vaultSettings.autoLockIdleSeconds().set(autoLockIdleSeconds);
|
||||||
|
vaultSettings.mountPoint().set(mountPoint);
|
||||||
return vaultSettings;
|
return vaultSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Path parseMountPoint(String mountPoint) {
|
||||||
|
try {
|
||||||
|
return Path.of(mountPoint);
|
||||||
|
} catch (InvalidPathException e) {
|
||||||
|
LOG.warn("Invalid string as mount point. Defaulting to null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) {
|
private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) {
|
||||||
try {
|
try {
|
||||||
return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase());
|
return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase());
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package org.cryptomator.common.settings;
|
|
||||||
|
|
||||||
public enum WebDavUrlScheme {
|
|
||||||
DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
|
|
||||||
WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");
|
|
||||||
|
|
||||||
private final String prefix;
|
|
||||||
private final String displayName;
|
|
||||||
|
|
||||||
WebDavUrlScheme(String prefix, String displayName) {
|
|
||||||
this.prefix = prefix;
|
|
||||||
this.displayName = displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPrefix() {
|
|
||||||
return prefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisplayName() {
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package org.cryptomator.common.vaults;
|
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
|
||||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
|
||||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public abstract class AbstractVolume implements Volume {
|
|
||||||
|
|
||||||
private final Iterable<MountPointChooser> choosers;
|
|
||||||
|
|
||||||
protected Path mountPoint;
|
|
||||||
private boolean cleanupRequired;
|
|
||||||
private MountPointChooser usedChooser;
|
|
||||||
|
|
||||||
public AbstractVolume(Iterable<MountPointChooser> choosers) {
|
|
||||||
this.choosers = choosers;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Path determineMountPoint() throws InvalidMountPointException {
|
|
||||||
var applicableChoosers = Iterables.filter(choosers, c -> c.isApplicable(this));
|
|
||||||
for (var chooser : applicableChoosers) {
|
|
||||||
Optional<Path> chosenPath = chooser.chooseMountPoint(this);
|
|
||||||
if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
this.cleanupRequired = chooser.prepare(this, chosenPath.get());
|
|
||||||
this.usedChooser = chooser;
|
|
||||||
return chosenPath.get();
|
|
||||||
}
|
|
||||||
throw new InvalidMountPointException(String.format("No feasible MountPoint found by choosers: %s", applicableChoosers));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void cleanupMountPoint() {
|
|
||||||
if (this.cleanupRequired) {
|
|
||||||
this.usedChooser.cleanup(this, this.mountPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Path> getMountPoint() {
|
|
||||||
return Optional.ofNullable(mountPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package org.cryptomator.common.vaults;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
|
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
import java.io.IOException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -39,7 +41,7 @@ public class AutoLocker {
|
|||||||
try {
|
try {
|
||||||
vault.lock(false);
|
vault.lock(false);
|
||||||
LOG.info("Autolocked {} after idle timeout", vault.getDisplayName());
|
LOG.info("Autolocked {} after idle timeout", vault.getDisplayName());
|
||||||
} catch (Volume.VolumeException | LockNotCompletedException e) {
|
} catch (UnmountFailedException | IOException e) {
|
||||||
LOG.error("Autolocking failed.", e);
|
LOG.error("Autolocking failed.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package org.cryptomator.common.vaults;
|
|
||||||
|
|
||||||
import javax.inject.Qualifier;
|
|
||||||
import java.lang.annotation.Documented;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
|
|
||||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
|
||||||
|
|
||||||
@Qualifier
|
|
||||||
@Documented
|
|
||||||
@Retention(RUNTIME)
|
|
||||||
@interface DefaultMountFlags {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package org.cryptomator.common.vaults;
|
|
||||||
|
|
||||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
|
||||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
|
||||||
import org.cryptomator.frontend.dokany.DokanyMountFailedException;
|
|
||||||
import org.cryptomator.frontend.dokany.Mount;
|
|
||||||
import org.cryptomator.frontend.dokany.MountFactory;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public class DokanyVolume extends AbstractVolume {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class);
|
|
||||||
|
|
||||||
private static final String FS_TYPE_NAME = "CryptomatorFS";
|
|
||||||
|
|
||||||
private final VaultSettings vaultSettings;
|
|
||||||
|
|
||||||
private Mount mount;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
|
||||||
super(choosers);
|
|
||||||
this.vaultSettings = vaultSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public VolumeImpl getImplementationType() {
|
|
||||||
return VolumeImpl.DOKANY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
|
||||||
this.mountPoint = determineMountPoint();
|
|
||||||
try {
|
|
||||||
this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction);
|
|
||||||
} catch (DokanyMountFailedException e) {
|
|
||||||
if (vaultSettings.getCustomMountPath().isPresent()) {
|
|
||||||
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
|
|
||||||
}
|
|
||||||
throw new VolumeException("Unable to mount Filesystem", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void reveal(Revealer revealer) throws VolumeException {
|
|
||||||
try {
|
|
||||||
mount.reveal(revealer::reveal);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new VolumeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unmount() throws VolumeException {
|
|
||||||
try {
|
|
||||||
mount.unmount();
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
throw new VolumeException("Unmount Failed.", e);
|
|
||||||
}
|
|
||||||
cleanupMountPoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unmountForced() {
|
|
||||||
mount.unmountForced();
|
|
||||||
cleanupMountPoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsForcedUnmount() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSupported() {
|
|
||||||
return DokanyVolume.isSupportedStatic();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MountPointRequirement getMountPointRequirement() {
|
|
||||||
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.EMPTY_MOUNT_POINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isSupportedStatic() {
|
|
||||||
return MountFactory.isApplicable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package org.cryptomator.common.vaults;
|
|
||||||
|
|
||||||
import com.google.common.collect.Iterators;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
|
||||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
|
||||||
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
|
|
||||||
import org.cryptomator.frontend.fuse.mount.FuseMountException;
|
|
||||||
import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
|
|
||||||
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
|
|
||||||
import org.cryptomator.frontend.fuse.mount.Mount;
|
|
||||||
import org.cryptomator.frontend.fuse.mount.Mounter;
|
|
||||||
|
|
||||||
import 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 Pattern NON_WHITESPACE_OR_QUOTED = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); // Thanks to https://stackoverflow.com/a/366532
|
|
||||||
|
|
||||||
private final VaultSettings vaultSettings;
|
|
||||||
|
|
||||||
private Mount mount;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public FuseVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
|
||||||
super(choosers);
|
|
||||||
this.vaultSettings = vaultSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
|
||||||
this.mountPoint = determineMountPoint();
|
|
||||||
mount(fs.getPath("/"), mountFlags, onExitAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void mount(Path root, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
|
|
||||||
try {
|
|
||||||
Mounter mounter = FuseMountFactory.getMounter();
|
|
||||||
EnvironmentVariables envVars = EnvironmentVariables.create() //
|
|
||||||
.withFlags(splitFlags(mountFlags)) //
|
|
||||||
.withMountPoint(mountPoint) //
|
|
||||||
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
|
|
||||||
.build();
|
|
||||||
this.mount = mounter.mount(root, envVars, onExitAction);
|
|
||||||
} catch (FuseMountException | FuseNotSupportedException e) {
|
|
||||||
throw new VolumeException("Unable to mount Filesystem", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String[] splitFlags(String str) {
|
|
||||||
List<String> flags = new ArrayList<>();
|
|
||||||
var matches = Iterators.peekingIterator(NON_WHITESPACE_OR_QUOTED.matcher(str).results().iterator());
|
|
||||||
while (matches.hasNext()) {
|
|
||||||
String flag = matches.next().group();
|
|
||||||
// check if flag is missing its argument:
|
|
||||||
if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(1) != null) { // next is "double quoted"
|
|
||||||
// next is "double quoted" and flag is missing its argument
|
|
||||||
flag += matches.next().group(1);
|
|
||||||
} else if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(2) != null) {
|
|
||||||
// next is 'single quoted' and flag is missing its argument
|
|
||||||
flag += matches.next().group(2);
|
|
||||||
}
|
|
||||||
flags.add(flag);
|
|
||||||
}
|
|
||||||
return flags.toArray(String[]::new);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void reveal(Revealer revealer) throws VolumeException {
|
|
||||||
try {
|
|
||||||
mount.reveal(revealer::reveal);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new VolumeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsForcedUnmount() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void unmountForced() throws VolumeException {
|
|
||||||
try {
|
|
||||||
mount.unmountForced();
|
|
||||||
} catch (FuseMountException e) {
|
|
||||||
throw new VolumeException(e);
|
|
||||||
}
|
|
||||||
cleanupMountPoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void unmount() throws VolumeException {
|
|
||||||
try {
|
|
||||||
mount.unmount();
|
|
||||||
} catch (FuseMountException e) {
|
|
||||||
throw new VolumeException(e);
|
|
||||||
}
|
|
||||||
cleanupMountPoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSupported() {
|
|
||||||
return FuseVolume.isSupportedStatic();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public VolumeImpl getImplementationType() {
|
|
||||||
return VolumeImpl.FUSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MountPointRequirement getMountPointRequirement() {
|
|
||||||
if (!SystemUtils.IS_OS_WINDOWS) {
|
|
||||||
return MountPointRequirement.EMPTY_MOUNT_POINT;
|
|
||||||
}
|
|
||||||
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.PARENT_NO_MOUNT_POINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isSupportedStatic() {
|
|
||||||
return FuseMountFactory.isFuseSupported();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package org.cryptomator.common.vaults;
|
|
||||||
|
|
||||||
public class LockNotCompletedException extends Exception {
|
|
||||||
|
|
||||||
public LockNotCompletedException(String reason) {
|
|
||||||
super(reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LockNotCompletedException(Throwable cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package org.cryptomator.common.vaults;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enumeration used to indicate the requirements for mounting a vault
|
|
||||||
* using a specific {@link Volume VolumeProvider}, e.g. {@link FuseVolume}.
|
|
||||||
*/
|
|
||||||
public enum MountPointRequirement {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Mountpoint needs to be a filesystem root and must not exist.
|
|
||||||
*/
|
|
||||||
UNUSED_ROOT_DIR,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No Mountpoint on the local filesystem required. (e.g. WebDAV)
|
|
||||||
*/
|
|
||||||
NONE,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A parent folder is required, but the actual Mountpoint must not exist.
|
|
||||||
*/
|
|
||||||
PARENT_NO_MOUNT_POINT,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A parent folder is required, but the actual Mountpoint may exist.
|
|
||||||
*/
|
|
||||||
PARENT_OPT_MOUNT_POINT,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The actual Mountpoint must exist and must be empty.
|
|
||||||
*/
|
|
||||||
EMPTY_MOUNT_POINT;
|
|
||||||
}
|
|
||||||
@@ -11,9 +11,10 @@ package org.cryptomator.common.vaults;
|
|||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
import org.cryptomator.common.Constants;
|
import org.cryptomator.common.Constants;
|
||||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
import org.cryptomator.common.Environment;
|
||||||
|
import org.cryptomator.common.mount.WindowsDriveLetters;
|
||||||
|
import org.cryptomator.common.settings.Settings;
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
||||||
@@ -22,30 +23,42 @@ import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
|
|||||||
import org.cryptomator.cryptolib.api.CryptoException;
|
import org.cryptomator.cryptolib.api.CryptoException;
|
||||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||||
|
import org.cryptomator.integrations.mount.Mount;
|
||||||
|
import org.cryptomator.integrations.mount.MountBuilder;
|
||||||
|
import org.cryptomator.integrations.mount.MountCapability;
|
||||||
|
import org.cryptomator.integrations.mount.MountFailedException;
|
||||||
|
import org.cryptomator.integrations.mount.MountService;
|
||||||
|
import org.cryptomator.integrations.mount.Mountpoint;
|
||||||
|
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Provider;
|
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.binding.ObjectBinding;
|
||||||
import javafx.beans.binding.StringBinding;
|
import javafx.beans.binding.StringBinding;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyStringProperty;
|
import javafx.beans.property.ReadOnlyStringProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER;
|
||||||
|
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR;
|
||||||
|
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_SYSTEM_CHOSEN_PATH;
|
||||||
|
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_WITHIN_EXISTING_PARENT;
|
||||||
|
|
||||||
@PerVault
|
@PerVault
|
||||||
public class Vault {
|
public class Vault {
|
||||||
|
|
||||||
@@ -53,12 +66,14 @@ public class Vault {
|
|||||||
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
|
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
|
||||||
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
|
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
private final Environment env;
|
||||||
|
private final Settings settings;
|
||||||
private final VaultSettings vaultSettings;
|
private final VaultSettings vaultSettings;
|
||||||
private final Provider<Volume> volumeProvider;
|
|
||||||
private final StringBinding defaultMountFlags;
|
|
||||||
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
|
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
|
||||||
private final VaultState state;
|
private final VaultState state;
|
||||||
private final ObjectProperty<Exception> lastKnownException;
|
private final ObjectProperty<Exception> lastKnownException;
|
||||||
|
private final ObservableValue<MountService> mountService;
|
||||||
|
private final ObservableValue<String> defaultMountFlags;
|
||||||
private final VaultConfigCache configCache;
|
private final VaultConfigCache configCache;
|
||||||
private final VaultStats stats;
|
private final VaultStats stats;
|
||||||
private final StringBinding displayablePath;
|
private final StringBinding displayablePath;
|
||||||
@@ -68,21 +83,23 @@ public class Vault {
|
|||||||
private final BooleanBinding missing;
|
private final BooleanBinding missing;
|
||||||
private final BooleanBinding needsMigration;
|
private final BooleanBinding needsMigration;
|
||||||
private final BooleanBinding unknownError;
|
private final BooleanBinding unknownError;
|
||||||
private final StringBinding accessPoint;
|
private final ObjectBinding<Mountpoint> mountPoint;
|
||||||
private final BooleanBinding accessPointPresent;
|
private final WindowsDriveLetters windowsDriveLetters;
|
||||||
private final BooleanProperty showingStats;
|
private final BooleanProperty showingStats;
|
||||||
|
|
||||||
private volatile Volume volume;
|
private AtomicReference<MountHandle> mountHandle = new AtomicReference<>(null);
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
Vault(VaultSettings vaultSettings, VaultConfigCache configCache, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
|
Vault(Environment env, Settings settings, VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, ObservableValue<MountService> mountService, VaultStats stats, WindowsDriveLetters windowsDriveLetters) {
|
||||||
|
this.env = env;
|
||||||
|
this.settings = settings;
|
||||||
this.vaultSettings = vaultSettings;
|
this.vaultSettings = vaultSettings;
|
||||||
this.configCache = configCache;
|
this.configCache = configCache;
|
||||||
this.volumeProvider = volumeProvider;
|
|
||||||
this.defaultMountFlags = defaultMountFlags;
|
|
||||||
this.cryptoFileSystem = cryptoFileSystem;
|
this.cryptoFileSystem = cryptoFileSystem;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.lastKnownException = lastKnownException;
|
this.lastKnownException = lastKnownException;
|
||||||
|
this.mountService = mountService;
|
||||||
|
this.defaultMountFlags = mountService.map(MountService::getDefaultMountFlags);
|
||||||
this.stats = stats;
|
this.stats = stats;
|
||||||
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
|
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
|
||||||
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
|
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
|
||||||
@@ -91,8 +108,8 @@ public class Vault {
|
|||||||
this.missing = Bindings.createBooleanBinding(this::isMissing, state);
|
this.missing = Bindings.createBooleanBinding(this::isMissing, state);
|
||||||
this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
|
this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
|
||||||
this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
|
this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
|
||||||
this.accessPoint = Bindings.createStringBinding(this::getAccessPoint, state);
|
this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state);
|
||||||
this.accessPointPresent = this.accessPoint.isNotEmpty();
|
this.windowsDriveLetters = windowsDriveLetters;
|
||||||
this.showingStats = new SimpleBooleanProperty(false);
|
this.showingStats = new SimpleBooleanProperty(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +159,46 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
private MountBuilder prepareMount(Path cryptoRoot) throws IOException {
|
||||||
|
var mountProvider = mountService.getValue();
|
||||||
|
var builder = mountProvider.forFileSystem(cryptoRoot);
|
||||||
|
|
||||||
|
for (var capability : mountProvider.capabilities()) {
|
||||||
|
switch (capability) {
|
||||||
|
case FILE_SYSTEM_NAME -> builder.setFileSystemName("crypto");
|
||||||
|
case LOOPBACK_PORT -> builder.setLoopbackPort(settings.port().get()); //TODO: move port from settings to vaultsettings?
|
||||||
|
case LOOPBACK_HOST_NAME -> builder.setLoopbackHostName("cryptomator-vault"); //TODO: Read from system property
|
||||||
|
case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode().get());
|
||||||
|
case MOUNT_FLAGS -> builder.setMountFlags(defaultMountFlags.getValue()); // TODO use custom mount flags (pre-populated with default mount flags)
|
||||||
|
case VOLUME_ID -> builder.setVolumeId(vaultSettings.getId());
|
||||||
|
case VOLUME_NAME -> builder.setVolumeName(vaultSettings.mountName().get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var userChosenMountPoint = vaultSettings.getMountPoint();
|
||||||
|
var defaultMountPointBase = env.getMountPointsDir().orElseThrow();
|
||||||
|
if (userChosenMountPoint == null) {
|
||||||
|
if (mountProvider.hasCapability(MOUNT_TO_SYSTEM_CHOSEN_PATH)) {
|
||||||
|
// no need to set a mount point
|
||||||
|
} else if (mountProvider.hasCapability(MOUNT_AS_DRIVE_LETTER)) {
|
||||||
|
builder.setMountpoint(windowsDriveLetters.getFirstDesiredAvailable().orElseThrow());
|
||||||
|
} else if (mountProvider.hasCapability(MOUNT_WITHIN_EXISTING_PARENT)) {
|
||||||
|
Files.createDirectories(defaultMountPointBase);
|
||||||
|
builder.setMountpoint(defaultMountPointBase);
|
||||||
|
} else if (mountProvider.hasCapability(MOUNT_TO_EXISTING_DIR) ) {
|
||||||
|
var mountPoint = defaultMountPointBase.resolve(vaultSettings.mountName().get());
|
||||||
|
Files.createDirectories(mountPoint);
|
||||||
|
builder.setMountpoint(mountPoint);
|
||||||
|
}
|
||||||
|
} else if (mountProvider.hasCapability(MOUNT_TO_EXISTING_DIR) || mountProvider.hasCapability(MOUNT_WITHIN_EXISTING_PARENT) || mountProvider.hasCapability(MOUNT_AS_DRIVE_LETTER)) {
|
||||||
|
// TODO: move the mount point away in case of MOUNT_WITHIN_EXISTING_PARENT?
|
||||||
|
builder.setMountpoint(userChosenMountPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, MountFailedException {
|
||||||
if (cryptoFileSystem.get() != null) {
|
if (cryptoFileSystem.get() != null) {
|
||||||
throw new IllegalStateException("Already unlocked.");
|
throw new IllegalStateException("Already unlocked.");
|
||||||
}
|
}
|
||||||
@@ -150,9 +206,10 @@ public class Vault {
|
|||||||
boolean success = false;
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
cryptoFileSystem.set(fs);
|
cryptoFileSystem.set(fs);
|
||||||
volume = volumeProvider.get();
|
var rootPath = fs.getRootDirectories().iterator().next();
|
||||||
volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit);
|
var supportsForcedUnmount = mountService.getValue().hasCapability(MountCapability.UNMOUNT_FORCED);
|
||||||
success = true;
|
var mountHandle = new MountHandle(prepareMount(rootPath).mount(), supportsForcedUnmount);
|
||||||
|
success = this.mountHandle.compareAndSet(null, mountHandle);
|
||||||
} finally {
|
} finally {
|
||||||
if (!success) {
|
if (!success) {
|
||||||
destroyCryptoFileSystem();
|
destroyCryptoFileSystem();
|
||||||
@@ -160,37 +217,28 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void lockOnVolumeExit(Throwable t) {
|
|
||||||
LOG.info("Unmounted vault '{}'", getDisplayName());
|
public synchronized void lock(boolean forced) throws UnmountFailedException, IOException {
|
||||||
destroyCryptoFileSystem();
|
var mountHandle = this.mountHandle.get();
|
||||||
state.set(VaultState.Value.LOCKED);
|
if (mountHandle == null) {
|
||||||
if (t != null) {
|
//TODO: noop or InvalidStateException?
|
||||||
LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException {
|
if (forced && mountHandle.supportsUnmountForced) {
|
||||||
//initiate unmount
|
mountHandle.mount.unmountForced();
|
||||||
if (forced && volume.supportsForcedUnmount()) {
|
|
||||||
volume.unmountForced();
|
|
||||||
} else {
|
} else {
|
||||||
volume.unmount();
|
mountHandle.mount.unmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
//wait for lockOnVolumeExit to be executed
|
|
||||||
try {
|
try {
|
||||||
boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS);
|
mountHandle.mount.close();
|
||||||
if (!locked) {
|
} finally {
|
||||||
throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress.");
|
destroyCryptoFileSystem();
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new LockNotCompletedException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
|
this.mountHandle.set(null);
|
||||||
volume.reveal(vaultRevealer);
|
LOG.info("Locked vault '{}'", getDisplayName());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ******************************************************************************
|
// ******************************************************************************
|
||||||
@@ -273,25 +321,13 @@ public class Vault {
|
|||||||
return vaultSettings.displayName().get();
|
return vaultSettings.displayName().get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringBinding accessPointProperty() {
|
public ObjectBinding<Mountpoint> mountPointProperty() {
|
||||||
return accessPoint;
|
return mountPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAccessPoint() {
|
public Mountpoint getMountPoint() {
|
||||||
if (state.getValue() == VaultState.Value.UNLOCKED) {
|
var handle = mountHandle.get();
|
||||||
assert volume != null;
|
return handle == null ? null : handle.mount.getMountpoint();
|
||||||
return volume.getMountPoint().orElse(Path.of("")).toString();
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public BooleanBinding accessPointPresentProperty() {
|
|
||||||
return accessPointPresent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAccessPointPresent() {
|
|
||||||
return accessPointPresent.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringBinding displayablePathProperty() {
|
public StringBinding displayablePathProperty() {
|
||||||
@@ -314,7 +350,7 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isShowingStats() {
|
public boolean isShowingStats() {
|
||||||
return accessPointPresent.get();
|
return mountHandle.get() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -343,18 +379,18 @@ public class Vault {
|
|||||||
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
|
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringBinding defaultMountFlagsProperty() {
|
public ObservableValue<String> defaultMountFlagsProperty() {
|
||||||
return defaultMountFlags;
|
return defaultMountFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDefaultMountFlags() {
|
public String getDefaultMountFlags() {
|
||||||
return defaultMountFlags.get();
|
return defaultMountFlags.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getEffectiveMountFlags() {
|
public String getEffectiveMountFlags() {
|
||||||
String mountFlags = vaultSettings.mountFlags().get();
|
String mountFlags = vaultSettings.mountFlags().get();
|
||||||
if (Strings.isNullOrEmpty(mountFlags)) {
|
if (Strings.isNullOrEmpty(mountFlags)) {
|
||||||
return getDefaultMountFlags();
|
return ""; //TODO: should the provider provide dem defaults??
|
||||||
} else {
|
} else {
|
||||||
return mountFlags;
|
return mountFlags;
|
||||||
}
|
}
|
||||||
@@ -372,10 +408,6 @@ public class Vault {
|
|||||||
return vaultSettings.getId();
|
return vaultSettings.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Volume> getVolume() {
|
|
||||||
return Optional.ofNullable(this.volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ******************************************************************************
|
// ******************************************************************************
|
||||||
// Hashcode / Equals
|
// Hashcode / Equals
|
||||||
// *******************************************************************************/
|
// *******************************************************************************/
|
||||||
@@ -394,7 +426,15 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* TODO: reactivate/ needed at all?
|
||||||
public boolean supportsForcedUnmount() {
|
public boolean supportsForcedUnmount() {
|
||||||
return volume.supportsForcedUnmount();
|
return volume.supportsForcedUnmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
private record MountHandle(Mount mount, boolean supportsUnmountForced) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,13 +8,12 @@ package org.cryptomator.common.vaults;
|
|||||||
import dagger.BindsInstance;
|
import dagger.BindsInstance;
|
||||||
import dagger.Subcomponent;
|
import dagger.Subcomponent;
|
||||||
import org.cryptomator.common.Nullable;
|
import org.cryptomator.common.Nullable;
|
||||||
import org.cryptomator.common.mountpoint.MountPointChooserModule;
|
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
@PerVault
|
@PerVault
|
||||||
@Subcomponent(modules = {VaultModule.class, MountPointChooserModule.class})
|
@Subcomponent(modules = {VaultModule.class})
|
||||||
public interface VaultComponent {
|
public interface VaultComponent {
|
||||||
|
|
||||||
Vault vault();
|
Vault vault();
|
||||||
|
|||||||
@@ -7,27 +7,14 @@ package org.cryptomator.common.vaults;
|
|||||||
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.Nullable;
|
import org.cryptomator.common.Nullable;
|
||||||
import org.cryptomator.common.settings.Settings;
|
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.binding.StringBinding;
|
|
||||||
import javafx.beans.binding.StringExpression;
|
|
||||||
import javafx.beans.property.BooleanProperty;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -48,130 +35,4 @@ public class VaultModule {
|
|||||||
return new SimpleObjectProperty<>(initialErrorCause);
|
return new SimpleObjectProperty<>(initialErrorCause);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
|
|
||||||
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
|
||||||
if (VolumeImpl.DOKANY == preferredImpl && dokanyVolume.isSupported()) {
|
|
||||||
return dokanyVolume;
|
|
||||||
} else if (VolumeImpl.FUSE == preferredImpl && fuseVolume.isSupported()) {
|
|
||||||
return fuseVolume;
|
|
||||||
} else {
|
|
||||||
if (VolumeImpl.WEBDAV != preferredImpl) {
|
|
||||||
LOG.warn("Using WebDAV, because {} is not supported.", preferredImpl.getDisplayName());
|
|
||||||
}
|
|
||||||
assert webDavVolume.isSupported();
|
|
||||||
return webDavVolume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@PerVault
|
|
||||||
@DefaultMountFlags
|
|
||||||
public StringBinding provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
|
||||||
ObjectProperty<VolumeImpl> preferredVolumeImpl = settings.preferredVolumeImpl();
|
|
||||||
StringExpression 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(StringExpression 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(StringExpression 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 totalBytesDecrypted = new SimpleLongProperty();
|
||||||
private final LongProperty filesRead = new SimpleLongProperty();
|
private final LongProperty filesRead = new SimpleLongProperty();
|
||||||
private final LongProperty filesWritten = 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<>();
|
private final ObjectProperty<Instant> lastActivity = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@@ -84,8 +82,6 @@ public class VaultStats {
|
|||||||
var oldAccessCount = filesRead.get() + filesWritten.get();
|
var oldAccessCount = filesRead.get() + filesWritten.get();
|
||||||
filesRead.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesRead).orElse(0L));
|
filesRead.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesRead).orElse(0L));
|
||||||
filesWritten.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesWritten).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();
|
var newAccessCount = filesRead.get() + filesWritten.get();
|
||||||
|
|
||||||
// check for any I/O activity
|
// check for any I/O activity
|
||||||
@@ -192,19 +188,6 @@ public class VaultStats {
|
|||||||
|
|
||||||
public long getFilesWritten() {return filesWritten.get();}
|
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() {
|
public ObjectProperty<Instant> lastActivityProperty() {
|
||||||
return lastActivity;
|
return lastActivity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
package org.cryptomator.common.vaults;
|
|
||||||
|
|
||||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.CompletionStage;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a Volume and uses it to mount an unlocked vault
|
|
||||||
*/
|
|
||||||
public interface Volume {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks in constant time whether this volume type is supported on the system running Cryptomator.
|
|
||||||
*
|
|
||||||
* @return true if this volume can be mounted
|
|
||||||
*/
|
|
||||||
boolean isSupported();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the corresponding enum type of the {@link VolumeImpl volume implementation ("VolumeImpl")} that is implemented by this Volume.
|
|
||||||
*
|
|
||||||
* @return the type of implementation as defined by the {@link VolumeImpl VolumeImpl enum}
|
|
||||||
*/
|
|
||||||
VolumeImpl getImplementationType();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param fs
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws IOException, VolumeException, InvalidMountPointException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reveals the mounted volume.
|
|
||||||
* <p>
|
|
||||||
* The given {@code revealer} might be used to do it, but not necessarily.
|
|
||||||
*
|
|
||||||
* @param revealer An object capable of revealing the location of the mounted vault to view the content (e.g. in the default file browser).
|
|
||||||
* @throws VolumeException
|
|
||||||
*/
|
|
||||||
void reveal(Revealer revealer) throws VolumeException;
|
|
||||||
|
|
||||||
void unmount() throws VolumeException;
|
|
||||||
|
|
||||||
Optional<Path> getMountPoint();
|
|
||||||
|
|
||||||
MountPointRequirement getMountPointRequirement();
|
|
||||||
|
|
||||||
// optional forced unmounting:
|
|
||||||
|
|
||||||
default boolean supportsForcedUnmount() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
default void unmountForced() throws VolumeException {
|
|
||||||
throw new VolumeException("Operation not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
static VolumeImpl[] getCurrentSupportedAdapters() {
|
|
||||||
return Stream.of(VolumeImpl.values()).filter(impl -> switch (impl) {
|
|
||||||
case WEBDAV -> WebDavVolume.isSupportedStatic();
|
|
||||||
case DOKANY -> DokanyVolume.isSupportedStatic();
|
|
||||||
case FUSE -> FuseVolume.isSupportedStatic();
|
|
||||||
}).toArray(VolumeImpl[]::new);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception thrown when a volume-specific command such as mount/unmount/reveal failed.
|
|
||||||
*/
|
|
||||||
class VolumeException extends Exception {
|
|
||||||
|
|
||||||
public VolumeException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public VolumeException(Throwable cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
public VolumeException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides and unifies the different Revealer implementations in the different nio-adapters.
|
|
||||||
*/
|
|
||||||
@FunctionalInterface
|
|
||||||
interface Revealer {
|
|
||||||
|
|
||||||
void reveal(Path p) throws VolumeException;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
package org.cryptomator.common.vaults;
|
|
||||||
|
|
||||||
|
|
||||||
import com.google.common.base.CharMatcher;
|
|
||||||
import org.cryptomator.common.Environment;
|
|
||||||
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 final Provider<WebDavServer> serverProvider;
|
|
||||||
private final VaultSettings vaultSettings;
|
|
||||||
private final Settings settings;
|
|
||||||
private final WindowsDriveLetters windowsDriveLetters;
|
|
||||||
private final Environment environment;
|
|
||||||
|
|
||||||
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, Environment environment) {
|
|
||||||
this.serverProvider = serverProvider;
|
|
||||||
this.vaultSettings = vaultSettings;
|
|
||||||
this.settings = settings;
|
|
||||||
this.windowsDriveLetters = windowsDriveLetters;
|
|
||||||
this.environment = environment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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() {
|
|
||||||
return environment.getLoopbackAlias().map(alias -> {
|
|
||||||
try {
|
|
||||||
var address = InetAddress.getByName(alias);
|
|
||||||
if (address.getHostAddress().equals("127.0.0.1")) {
|
|
||||||
return alias;
|
|
||||||
}
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
//no-op
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}).orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanup() {
|
|
||||||
if (servlet != null) {
|
|
||||||
servlet.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSupported() {
|
|
||||||
return WebDavVolume.isSupportedStatic();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public VolumeImpl getImplementationType() {
|
|
||||||
return VolumeImpl.WEBDAV;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsForcedUnmount() {
|
|
||||||
return mount != null && mount.forced().isPresent();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static boolean isSupportedStatic() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.common.vaults;
|
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
|
||||||
import com.google.common.collect.Sets;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import java.nio.file.FileSystems;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
import java.util.stream.StreamSupport;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public final class WindowsDriveLetters {
|
|
||||||
|
|
||||||
private static final Set<String> A_TO_Z;
|
|
||||||
|
|
||||||
static {
|
|
||||||
try (IntStream stream = IntStream.rangeClosed('A', 'Z')) {
|
|
||||||
A_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(ImmutableSet.toImmutableSet());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public WindowsDriveLetters() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getAllDriveLetters() {
|
|
||||||
return A_TO_Z;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getOccupiedDriveLetters() {
|
|
||||||
if (!SystemUtils.IS_OS_WINDOWS) {
|
|
||||||
return Set.of();
|
|
||||||
} else {
|
|
||||||
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
|
|
||||||
return StreamSupport.stream(rootDirs.spliterator(), false).map(p -> p.toString().substring(0, 1)).collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getAvailableDriveLetters() {
|
|
||||||
return Sets.difference(getAllDriveLetters(), getOccupiedDriveLetters());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<String> getAvailableDriveLetter() {
|
|
||||||
return getAvailableDriveLetters().stream().findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Path> getAvailableDriveLetterPath() {
|
|
||||||
return getAvailableDriveLetter().map(this::toPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skips A and B and only returns them if all other are occupied.
|
|
||||||
*
|
|
||||||
* @return an Optional containing either the letter of a free drive letter or empty, if none is available
|
|
||||||
*/
|
|
||||||
public Optional<String> getDesiredAvailableDriveLetter() {
|
|
||||||
var availableDriveLetters = getAvailableDriveLetters();
|
|
||||||
var optString = availableDriveLetters.stream().filter(s -> !(s.equals("A") || s.equals("B"))).findFirst();
|
|
||||||
return optString.or(() -> availableDriveLetters.stream().findFirst());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skips A and B and only returns them if all other are occupied.
|
|
||||||
*
|
|
||||||
* @return an Optional containing either the path to a free drive letter or empty, if none is available
|
|
||||||
*/
|
|
||||||
public Optional<Path> getDesiredAvailableDriveLetterPath() {
|
|
||||||
return getDesiredAvailableDriveLetter().map(this::toPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path toPath(String driveLetter) {
|
|
||||||
return Path.of(driveLetter + ":\\");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+12
-21
@@ -2,20 +2,19 @@ package org.cryptomator.ui.addvaultwizard;
|
|||||||
|
|
||||||
import dagger.Lazy;
|
import dagger.Lazy;
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
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.Vault;
|
||||||
import org.cryptomator.common.vaults.VaultListManager;
|
import org.cryptomator.common.vaults.VaultListManager;
|
||||||
import org.cryptomator.integrations.uiappearance.Theme;
|
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.common.FxmlFile;
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
import org.cryptomator.ui.common.FxmlScene;
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
import org.cryptomator.ui.fxapp.FxApplicationStyle;
|
|
||||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
@@ -24,7 +23,6 @@ import javafx.stage.Stage;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_GLOB;
|
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 ObjectProperty<Vault> vault;
|
||||||
private final VaultListManager vaultListManager;
|
private final VaultListManager vaultListManager;
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
private final ObservableValue<Image> screenshot;
|
private final Settings settings;
|
||||||
|
|
||||||
|
private Image screenshot;
|
||||||
|
|
||||||
@Inject
|
@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.window = window;
|
||||||
this.welcomeScene = welcomeScene;
|
this.welcomeScene = welcomeScene;
|
||||||
this.successScene = successScene;
|
this.successScene = successScene;
|
||||||
@@ -54,20 +54,16 @@ public class ChooseExistingVaultController implements FxController {
|
|||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.vaultListManager = vaultListManager;
|
this.vaultListManager = vaultListManager;
|
||||||
this.resourceBundle = resourceBundle;
|
this.resourceBundle = resourceBundle;
|
||||||
this.screenshot = applicationStyle.appliedThemeProperty().map(this::selectScreenshot);
|
this.settings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Image selectScreenshot(Theme theme) {
|
@FXML
|
||||||
String imageResourcePath;
|
public void initialize() {
|
||||||
if (SystemUtils.IS_OS_MAC) {
|
if (SystemUtils.IS_OS_MAC) {
|
||||||
imageResourcePath = switch (theme) {
|
this.screenshot = new Image(getClass().getResource("/img/select-masterkey-mac"+(UiTheme.LIGHT == settings.theme().get()? "":"-dark")+".png").toString());
|
||||||
case LIGHT -> "/img/select-masterkey-mac.png";
|
|
||||||
case DARK -> "/img/select-masterkey-mac-dark.png";
|
|
||||||
};
|
|
||||||
} else {
|
} 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
|
@FXML
|
||||||
@@ -96,13 +92,8 @@ public class ChooseExistingVaultController implements FxController {
|
|||||||
|
|
||||||
/* Getter */
|
/* Getter */
|
||||||
|
|
||||||
public ObservableValue<Image> screenshotProperty() {
|
public Image getScreenshot() {
|
||||||
return screenshot;
|
return screenshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Image getScreenshot() {
|
|
||||||
return screenshot.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package org.cryptomator.ui.common;
|
|
||||||
|
|
||||||
import dagger.Lazy;
|
|
||||||
import org.cryptomator.common.vaults.Volume;
|
|
||||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javafx.application.Application;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
@FxApplicationScoped
|
|
||||||
public class HostServiceRevealer implements Volume.Revealer {
|
|
||||||
|
|
||||||
private final Lazy<Application> application;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public HostServiceRevealer(Lazy<Application> application) {
|
|
||||||
this.application = application;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void reveal(Path p) throws Volume.VolumeException {
|
|
||||||
application.get().getHostServices().showDocument(p.toUri().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
package org.cryptomator.ui.common;
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
import dagger.Lazy;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.common.vaults.VaultState;
|
import org.cryptomator.common.vaults.VaultState;
|
||||||
import org.cryptomator.common.vaults.Volume;
|
import org.cryptomator.integrations.mount.Mountpoint;
|
||||||
|
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.application.HostServices;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
@@ -24,13 +28,13 @@ public class VaultService {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
|
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
|
||||||
|
|
||||||
|
private final Lazy<Application> application;
|
||||||
private final ExecutorService executorService;
|
private final ExecutorService executorService;
|
||||||
private final HostServiceRevealer vaultRevealer;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public VaultService(ExecutorService executorService, HostServiceRevealer vaultRevealer) {
|
public VaultService(Lazy<Application> application, ExecutorService executorService) {
|
||||||
|
this.application = application;
|
||||||
this.executorService = executorService;
|
this.executorService = executorService;
|
||||||
this.vaultRevealer = vaultRevealer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reveal(Vault vault) {
|
public void reveal(Vault vault) {
|
||||||
@@ -43,7 +47,7 @@ public class VaultService {
|
|||||||
* @param vault The vault to reveal
|
* @param vault The vault to reveal
|
||||||
*/
|
*/
|
||||||
public Task<Vault> createRevealTask(Vault vault) {
|
public Task<Vault> createRevealTask(Vault vault) {
|
||||||
Task<Vault> task = new RevealVaultTask(vault, vaultRevealer);
|
Task<Vault> task = new RevealVaultTask(vault, application.get().getHostServices());
|
||||||
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName()));
|
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName()));
|
||||||
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException()));
|
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException()));
|
||||||
return task;
|
return task;
|
||||||
@@ -106,22 +110,21 @@ public class VaultService {
|
|||||||
private static class RevealVaultTask extends Task<Vault> {
|
private static class RevealVaultTask extends Task<Vault> {
|
||||||
|
|
||||||
private final Vault vault;
|
private final Vault vault;
|
||||||
private final Volume.Revealer revealer;
|
private final HostServices hostServices;
|
||||||
|
|
||||||
/**
|
public RevealVaultTask(Vault vault, HostServices hostServices) {
|
||||||
* @param vault The vault to lock
|
|
||||||
* @param revealer The object to use to show the vault content to the user.
|
|
||||||
*/
|
|
||||||
public RevealVaultTask(Vault vault, Volume.Revealer revealer) {
|
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.revealer = revealer;
|
this.hostServices = hostServices;
|
||||||
|
|
||||||
setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException()));
|
setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Vault call() throws Volume.VolumeException {
|
protected Vault call() {
|
||||||
vault.reveal(revealer);
|
switch (vault.getMountPoint()) {
|
||||||
|
case null -> LOG.warn("Not currently mounted");
|
||||||
|
case Mountpoint.WithPath m -> hostServices.showDocument(m.uri().toString());
|
||||||
|
case Mountpoint.WithUri m -> LOG.info("Vault mounted at {}", m.uri()); // TODO show in UI?
|
||||||
|
}
|
||||||
return vault;
|
return vault;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +183,7 @@ public class VaultService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Vault call() throws Volume.VolumeException, LockNotCompletedException {
|
protected Vault call() throws UnmountFailedException, IOException {
|
||||||
vault.lock(forced);
|
vault.lock(forced);
|
||||||
return vault;
|
return vault;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ public enum FontAwesome5Icon {
|
|||||||
FILE("\uF15B"), //
|
FILE("\uF15B"), //
|
||||||
FILE_IMPORT("\uF56F"), //
|
FILE_IMPORT("\uF56F"), //
|
||||||
FOLDER_OPEN("\uF07C"), //
|
FOLDER_OPEN("\uF07C"), //
|
||||||
FUNNEL("\uF0B0"), //
|
|
||||||
HAND_HOLDING_HEART("\uF4BE"), //
|
HAND_HOLDING_HEART("\uF4BE"), //
|
||||||
HEART("\uF004"), //
|
HEART("\uF004"), //
|
||||||
HDD("\uF0A0"), //
|
HDD("\uF0A0"), //
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -26,10 +24,9 @@ public class FxApplicationStyle {
|
|||||||
private final Optional<UiAppearanceProvider> appearanceProvider;
|
private final Optional<UiAppearanceProvider> appearanceProvider;
|
||||||
private final LicenseHolder licenseHolder;
|
private final LicenseHolder licenseHolder;
|
||||||
private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
|
private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
|
||||||
private final ObjectProperty<Theme> appliedTheme = new SimpleObjectProperty<>(Theme.LIGHT);
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FxApplicationStyle(Settings settings, Optional<UiAppearanceProvider> appearanceProvider, LicenseHolder licenseHolder) {
|
public FxApplicationStyle(Settings settings, Optional<UiAppearanceProvider> appearanceProvider, LicenseHolder licenseHolder){
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.appearanceProvider = appearanceProvider;
|
this.appearanceProvider = appearanceProvider;
|
||||||
this.licenseHolder = licenseHolder;
|
this.licenseHolder = licenseHolder;
|
||||||
@@ -94,7 +91,6 @@ public class FxApplicationStyle {
|
|||||||
} else {
|
} else {
|
||||||
Application.setUserAgentStylesheet(stylesheet.toString());
|
Application.setUserAgentStylesheet(stylesheet.toString());
|
||||||
appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.LIGHT));
|
appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.LIGHT));
|
||||||
appliedTheme.set(Theme.LIGHT);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,11 +103,6 @@ public class FxApplicationStyle {
|
|||||||
} else {
|
} else {
|
||||||
Application.setUserAgentStylesheet(stylesheet.toString());
|
Application.setUserAgentStylesheet(stylesheet.toString());
|
||||||
appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.DARK));
|
appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.DARK));
|
||||||
appliedTheme.set(Theme.DARK);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectProperty<Theme> appliedThemeProperty() {
|
|
||||||
return appliedTheme;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ package org.cryptomator.ui.fxapp;
|
|||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import org.cryptomator.common.ShutdownHook;
|
import org.cryptomator.common.ShutdownHook;
|
||||||
import org.cryptomator.common.settings.Settings;
|
import org.cryptomator.common.settings.Settings;
|
||||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.common.vaults.VaultState;
|
import org.cryptomator.common.vaults.VaultState;
|
||||||
import org.cryptomator.common.vaults.Volume;
|
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||||
import org.cryptomator.ui.common.VaultService;
|
import org.cryptomator.ui.common.VaultService;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -18,6 +17,7 @@ import javafx.collections.ObservableList;
|
|||||||
import java.awt.Desktop;
|
import java.awt.Desktop;
|
||||||
import java.awt.desktop.QuitResponse;
|
import java.awt.desktop.QuitResponse;
|
||||||
import java.awt.desktop.QuitStrategy;
|
import java.awt.desktop.QuitStrategy;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.EventObject;
|
import java.util.EventObject;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -128,10 +128,8 @@ public class FxApplicationTerminator {
|
|||||||
if (vault.isUnlocked()) {
|
if (vault.isUnlocked()) {
|
||||||
try {
|
try {
|
||||||
vault.lock(true);
|
vault.lock(true);
|
||||||
} catch (Volume.VolumeException e) {
|
} catch (UnmountFailedException | IOException e) {
|
||||||
LOG.error("Failed to unmount vault " + vault.getPath(), e);
|
LOG.error("Failed to unmount vault " + vault.getPath(), e);
|
||||||
} catch (LockNotCompletedException e) {
|
|
||||||
LOG.error("Failed to lock vault " + vault.getPath(), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,34 +8,15 @@ import org.cryptomator.ui.common.FxController;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javafx.beans.binding.Binding;
|
import javafx.beans.binding.Binding;
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.binding.BooleanBinding;
|
|
||||||
import javafx.beans.binding.BooleanExpression;
|
import javafx.beans.binding.BooleanExpression;
|
||||||
import javafx.beans.property.BooleanProperty;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.ChoiceBox;
|
|
||||||
import javafx.scene.control.ListView;
|
import javafx.scene.control.ListView;
|
||||||
import javafx.scene.input.Clipboard;
|
|
||||||
import javafx.scene.input.ClipboardContent;
|
|
||||||
import javafx.util.StringConverter;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.cryptomator.cryptofs.health.api.DiagnosticResult.Severity;
|
|
||||||
import static org.cryptomator.ui.health.Result.FixState.FIXABLE;
|
|
||||||
import static org.cryptomator.ui.health.Result.FixState.FIXED;
|
|
||||||
import static org.cryptomator.ui.health.Result.FixState.FIXING;
|
|
||||||
import static org.cryptomator.ui.health.Result.FixState.FIX_FAILED;
|
|
||||||
import static org.cryptomator.ui.health.Result.FixState.NOT_FIXABLE;
|
|
||||||
|
|
||||||
@HealthCheckScoped
|
@HealthCheckScoped
|
||||||
public class CheckDetailController implements FxController {
|
public class CheckDetailController implements FxController {
|
||||||
|
|
||||||
@@ -54,48 +35,30 @@ public class CheckDetailController implements FxController {
|
|||||||
private final Binding<Number> countOfCritSeverity;
|
private final Binding<Number> countOfCritSeverity;
|
||||||
private final Binding<Boolean> warnOrCritsExist;
|
private final Binding<Boolean> warnOrCritsExist;
|
||||||
private final ResultListCellFactory resultListCellFactory;
|
private final ResultListCellFactory resultListCellFactory;
|
||||||
private final ResultFixApplier resultFixApplier;
|
|
||||||
private final ResourceBundle resourceBundle;
|
|
||||||
|
|
||||||
private final BooleanProperty fixAllInfoResultsExecuted;
|
|
||||||
private final BooleanBinding fixAllInfoResultsPossible;
|
|
||||||
private final ObjectProperty<Predicate<Result>> resultsFilter;
|
|
||||||
|
|
||||||
public ListView<Result> resultsListView;
|
public ListView<Result> resultsListView;
|
||||||
public ChoiceBox<DiagnosticResult.Severity> severityChoiceBox;
|
|
||||||
public ChoiceBox<Result.FixState> fixStateChoiceBox;
|
|
||||||
private Subscription resultSubscription;
|
private Subscription resultSubscription;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public CheckDetailController(ObjectProperty<Check> selectedTask, ResultListCellFactory resultListCellFactory, ResultFixApplier resultFixApplier, ResourceBundle resourceBundle) {
|
public CheckDetailController(ObjectProperty<Check> selectedTask, ResultListCellFactory resultListCellFactory) {
|
||||||
this.resultListCellFactory = resultListCellFactory;
|
this.resultListCellFactory = resultListCellFactory;
|
||||||
this.resultFixApplier = resultFixApplier;
|
|
||||||
this.resourceBundle = resourceBundle;
|
|
||||||
this.results = EasyBind.wrapList(FXCollections.observableArrayList());
|
this.results = EasyBind.wrapList(FXCollections.observableArrayList());
|
||||||
this.check = selectedTask;
|
this.check = selectedTask;
|
||||||
this.checkState = selectedTask.flatMap(Check::stateProperty);
|
this.checkState = selectedTask.flatMap(Check::stateProperty);
|
||||||
this.checkName = selectedTask.map(Check::getName).orElse("");
|
this.checkName = selectedTask.map(Check::getName).orElse("");
|
||||||
this.checkRunning = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.RUNNING::equals).orElse(false));
|
this.checkRunning = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.RUNNING::equals).orElse(false));
|
||||||
this.checkScheduled = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SCHEDULED::equals).orElse(false));
|
this.checkScheduled = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SCHEDULED::equals).orElse(false));
|
||||||
this.checkSkipped = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SKIPPED::equals).orElse(false));
|
this.checkSkipped =BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SKIPPED::equals).orElse(false));
|
||||||
this.checkSucceeded = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SUCCEEDED::equals).orElse(false));
|
this.checkSucceeded = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SUCCEEDED::equals).orElse(false));
|
||||||
this.checkFailed = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.ERROR::equals).orElse(false));
|
this.checkFailed = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.ERROR::equals).orElse(false));
|
||||||
this.checkCancelled = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.CANCELLED::equals).orElse(false));
|
this.checkCancelled = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.CANCELLED::equals).orElse(false));
|
||||||
this.checkFinished = checkSucceeded.or(checkFailed).or(checkCancelled);
|
this.checkFinished = checkSucceeded.or(checkFailed).or(checkCancelled);
|
||||||
this.countOfWarnSeverity = results.reduce(countSeverity(Severity.WARN));
|
this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN));
|
||||||
this.countOfCritSeverity = results.reduce(countSeverity(Severity.CRITICAL));
|
this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL));
|
||||||
this.warnOrCritsExist = EasyBind.combine(checkSucceeded, countOfWarnSeverity, countOfCritSeverity, (suceeded, warns, crits) -> suceeded && (warns.longValue() > 0 || crits.longValue() > 0));
|
this.warnOrCritsExist = EasyBind.combine(checkSucceeded, countOfWarnSeverity, countOfCritSeverity, (suceeded, warns, crits) -> suceeded && (warns.longValue() > 0 || crits.longValue() > 0) );
|
||||||
this.fixAllInfoResultsExecuted = new SimpleBooleanProperty(false);
|
|
||||||
this.fixAllInfoResultsPossible = Bindings.createBooleanBinding(() -> results.stream().anyMatch(this::isFixableInfoResult), results) //
|
|
||||||
.and(fixAllInfoResultsExecuted.not());
|
|
||||||
this.resultsFilter = new SimpleObjectProperty<>(r -> true);
|
|
||||||
selectedTask.addListener(this::selectedTaskChanged);
|
selectedTask.addListener(this::selectedTaskChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFixableInfoResult(Result r) {
|
|
||||||
return r.diagnosis().getSeverity() == Severity.INFO && r.getState() == FIXABLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void selectedTaskChanged(ObservableValue<? extends Check> observable, Check oldValue, Check newValue) {
|
private void selectedTaskChanged(ObservableValue<? extends Check> observable, Check oldValue, Check newValue) {
|
||||||
if (resultSubscription != null) {
|
if (resultSubscription != null) {
|
||||||
resultSubscription.unsubscribe();
|
resultSubscription.unsubscribe();
|
||||||
@@ -103,8 +66,6 @@ public class CheckDetailController implements FxController {
|
|||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
resultSubscription = EasyBind.bindContent(results, newValue.getResults());
|
resultSubscription = EasyBind.bindContent(results, newValue.getResults());
|
||||||
}
|
}
|
||||||
severityChoiceBox.setValue(null);
|
|
||||||
fixStateChoiceBox.setValue(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<Stream<? extends Result>, Long> countSeverity(DiagnosticResult.Severity severity) {
|
private Function<Stream<? extends Result>, Long> countSeverity(DiagnosticResult.Severity severity) {
|
||||||
@@ -113,110 +74,8 @@ public class CheckDetailController implements FxController {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
resultsListView.setItems(results.filtered(resultsFilter));
|
resultsListView.setItems(results);
|
||||||
resultsListView.setCellFactory(resultListCellFactory);
|
resultsListView.setCellFactory(resultListCellFactory);
|
||||||
|
|
||||||
severityChoiceBox.getItems().add(null);
|
|
||||||
severityChoiceBox.getItems().addAll(Arrays.stream(DiagnosticResult.Severity.values()).toList());
|
|
||||||
severityChoiceBox.setConverter(new SeverityStringifier());
|
|
||||||
severityChoiceBox.setValue(null);
|
|
||||||
|
|
||||||
fixStateChoiceBox.getItems().add(null);
|
|
||||||
fixStateChoiceBox.getItems().addAll(Arrays.stream(Result.FixState.values()).toList());
|
|
||||||
fixStateChoiceBox.setConverter(new FixStateStringifier());
|
|
||||||
fixStateChoiceBox.setValue(null);
|
|
||||||
|
|
||||||
resultsFilter.bind(Bindings.createObjectBinding(() -> this::filterResults, severityChoiceBox.valueProperty(), fixStateChoiceBox.valueProperty()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean filterResults(Result r) {
|
|
||||||
var desiredFixState = fixStateChoiceBox.getValue();
|
|
||||||
var desiredSeverity = severityChoiceBox.getValue();
|
|
||||||
return (desiredFixState == null || r.getState() == desiredFixState) && (desiredSeverity == null || r.diagnosis().getSeverity() == desiredSeverity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
public void fixAllInfoResults() {
|
|
||||||
fixAllInfoResultsExecuted.setValue(true);
|
|
||||||
results.stream().filter(this::isFixableInfoResult).forEach(resultFixApplier::fix);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
public void copyResultDetails() {
|
|
||||||
var result = resultsListView.getSelectionModel().getSelectedItem();
|
|
||||||
if (result != null) {
|
|
||||||
ClipboardContent clipboardContent = new ClipboardContent();
|
|
||||||
clipboardContent.putString(result.diagnosis().toString());
|
|
||||||
Clipboard.getSystemClipboard().setContent(clipboardContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Internal classes -- */
|
|
||||||
|
|
||||||
class SeverityStringifier extends StringConverter<Severity> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString(Severity object) {
|
|
||||||
if (object == null) {
|
|
||||||
return resourceBundle.getString("health.result.severityFilter.all");
|
|
||||||
}
|
|
||||||
return switch (object) {
|
|
||||||
case GOOD -> resourceBundle.getString("health.result.severityFilter.good");
|
|
||||||
case INFO -> resourceBundle.getString("health.result.severityFilter.info");
|
|
||||||
case WARN -> resourceBundle.getString("health.result.severityFilter.warn");
|
|
||||||
case CRITICAL -> resourceBundle.getString("health.result.severityFilter.crit");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Severity fromString(String string) {
|
|
||||||
if (resourceBundle.getString("health.result.severityFilter.good").equals(string)) {
|
|
||||||
return Severity.GOOD;
|
|
||||||
} else if (resourceBundle.getString("health.result.severityFilter.info").equals(string)) {
|
|
||||||
return Severity.INFO;
|
|
||||||
} else if (resourceBundle.getString("health.result.severityFilter.warn").equals(string)) {
|
|
||||||
return Severity.WARN;
|
|
||||||
} else if (resourceBundle.getString("health.result.severityFilter.crit").equals(string)) {
|
|
||||||
return Severity.CRITICAL;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FixStateStringifier extends StringConverter<Result.FixState> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString(Result.FixState object) {
|
|
||||||
if (object == null) {
|
|
||||||
return resourceBundle.getString("health.result.fixStateFilter.all");
|
|
||||||
}
|
|
||||||
return switch (object) {
|
|
||||||
case FIXABLE -> resourceBundle.getString("health.result.fixStateFilter.fixable");
|
|
||||||
case NOT_FIXABLE -> resourceBundle.getString("health.result.fixStateFilter.notFixable");
|
|
||||||
case FIXING -> resourceBundle.getString("health.result.fixStateFilter.fixing");
|
|
||||||
case FIXED -> resourceBundle.getString("health.result.fixStateFilter.fixed");
|
|
||||||
case FIX_FAILED -> resourceBundle.getString("health.result.fixStateFilter.fixFailed");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result.FixState fromString(String string) {
|
|
||||||
if (resourceBundle.getString("health.result.fixStateFilter.fixable").equals(string)) {
|
|
||||||
return FIXABLE;
|
|
||||||
} else if (resourceBundle.getString("health.result.fixStateFilter.notFixable").equals(string)) {
|
|
||||||
return NOT_FIXABLE;
|
|
||||||
} else if (resourceBundle.getString("health.result.fixStateFilter.fixing").equals(string)) {
|
|
||||||
return FIXING;
|
|
||||||
} else if (resourceBundle.getString("health.result.fixStateFilter.fixed").equals(string)) {
|
|
||||||
return FIXED;
|
|
||||||
} else if (resourceBundle.getString("health.result.fixStateFilter.fixFailed").equals(string)) {
|
|
||||||
return FIX_FAILED;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Getter/Setter */
|
/* Getter/Setter */
|
||||||
@@ -316,12 +175,4 @@ public class CheckDetailController implements FxController {
|
|||||||
public Check getCheck() {
|
public Check getCheck() {
|
||||||
return check.get();
|
return check.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableValue<Boolean> fixAllInfoResultsPossibleProperty() {
|
|
||||||
return fixAllInfoResultsPossible;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getFixAllInfoResultsPossible() {
|
|
||||||
return fixAllInfoResultsPossible.getValue();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ public class CheckExecutor {
|
|||||||
try (var masterkeyClone = masterkey.copy(); //
|
try (var masterkeyClone = masterkey.copy(); //
|
||||||
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
|
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
|
||||||
c.getHealthCheck().check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> {
|
c.getHealthCheck().check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> {
|
||||||
Platform.runLater(() -> c.getResults().add(Result.create(diagnosis, vaultPath, vaultConfig, masterkeyClone, cryptor)));
|
Platform.runLater(() -> c.getResults().add(Result.create(diagnosis)));
|
||||||
highestResultSeverity = Comparators.max(highestResultSeverity, diagnosis.getSeverity());
|
highestResultSeverity = Comparators.max(highestResultSeverity, diagnosis.getSeverity());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class CheckStateIconView extends FontAwesome5IconView {
|
|||||||
this.severity = EasyBind.wrapNullable(check).mapObservable(Check::highestResultSeverityProperty).asOrdinary();
|
this.severity = EasyBind.wrapNullable(check).mapObservable(Check::highestResultSeverityProperty).asOrdinary();
|
||||||
this.glyph.bind(Bindings.createObjectBinding(this::glyphForState, state, severity));
|
this.glyph.bind(Bindings.createObjectBinding(this::glyphForState, state, severity));
|
||||||
this.subscriptions = List.of( //
|
this.subscriptions = List.of( //
|
||||||
EasyBind.includeWhen(getStyleClass(), "glyph-icon-muted", Bindings.equal(state, Check.CheckState.SKIPPED).or(Bindings.equal(state, Check.CheckState.CANCELLED)).or(Bindings.equal(severity, DiagnosticResult.Severity.INFO))), //
|
EasyBind.includeWhen(getStyleClass(), "glyph-icon-muted", Bindings.equal(state, Check.CheckState.SKIPPED).or(Bindings.equal(state, Check.CheckState.CANCELLED))), //
|
||||||
EasyBind.includeWhen(getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), //
|
EasyBind.includeWhen(getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), //
|
||||||
EasyBind.includeWhen(getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN).or(Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL))), //
|
EasyBind.includeWhen(getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN).or(Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL))), //
|
||||||
EasyBind.includeWhen(getStyleClass(), "glyph-icon-red", Bindings.equal(state, Check.CheckState.ERROR)) //
|
EasyBind.includeWhen(getStyleClass(), "glyph-icon-red", Bindings.equal(state, Check.CheckState.ERROR)) //
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
package org.cryptomator.ui.health;
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
import org.cryptomator.cryptofs.VaultConfig;
|
|
||||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||||
import org.cryptomator.cryptolib.api.Cryptor;
|
|
||||||
import org.cryptomator.cryptolib.api.Masterkey;
|
|
||||||
|
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
record Result(DiagnosticResult diagnosis, ObjectProperty<FixState> fixState) {
|
record Result(DiagnosticResult diagnosis, ObjectProperty<FixState> fixState) {
|
||||||
|
|
||||||
@@ -20,8 +16,8 @@ record Result(DiagnosticResult diagnosis, ObjectProperty<FixState> fixState) {
|
|||||||
FIX_FAILED
|
FIX_FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Result create(DiagnosticResult diagnosis, Path vaultPath, VaultConfig config, Masterkey masterkey, Cryptor cryptor) {
|
public static Result create(DiagnosticResult diagnosis) {
|
||||||
FixState initialState = diagnosis.getFix(vaultPath, config, masterkey, cryptor).map( _f -> FixState.FIXABLE).orElse(FixState.NOT_FIXABLE);
|
FixState initialState = diagnosis.getSeverity() == DiagnosticResult.Severity.WARN ? FixState.FIXABLE : FixState.NOT_FIXABLE;
|
||||||
return new Result(diagnosis, new SimpleObjectProperty<>(initialState));
|
return new Result(diagnosis, new SimpleObjectProperty<>(initialState));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
@HealthCheckScoped
|
@HealthCheckScoped
|
||||||
class ResultFixApplier {
|
class ResultFixApplier {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class);
|
|
||||||
|
|
||||||
private final Path vaultPath;
|
private final Path vaultPath;
|
||||||
private final SecureRandom csprng;
|
private final SecureRandom csprng;
|
||||||
private final Masterkey masterkey;
|
private final Masterkey masterkey;
|
||||||
@@ -42,34 +40,25 @@ class ResultFixApplier {
|
|||||||
|
|
||||||
public CompletionStage<Void> fix(Result result) {
|
public CompletionStage<Void> fix(Result result) {
|
||||||
Preconditions.checkArgument(result.getState() == Result.FixState.FIXABLE);
|
Preconditions.checkArgument(result.getState() == Result.FixState.FIXABLE);
|
||||||
return CompletableFuture.runAsync(() -> result.setState(Result.FixState.FIXING), Platform::runLater) //
|
result.setState(Result.FixState.FIXING);
|
||||||
.thenRunAsync(() -> fix(result.diagnosis()), sequentialExecutor) //
|
return CompletableFuture.runAsync(() -> fix(result.diagnosis()), sequentialExecutor)
|
||||||
.whenCompleteAsync((unused, throwable) -> {
|
.whenCompleteAsync((unused, throwable) -> {
|
||||||
final Result.FixState s;
|
var fixed = throwable == null ? Result.FixState.FIXED : Result.FixState.FIX_FAILED;
|
||||||
if (throwable == null) {
|
result.setState(fixed);
|
||||||
LOG.debug("Fix for {} applied successful.", result.diagnosis().getClass().getName());
|
|
||||||
s = Result.FixState.FIXED;
|
|
||||||
} else {
|
|
||||||
LOG.error("Failed to apply fix for {}", result.diagnosis().getClass().getName(), throwable);
|
|
||||||
s = Result.FixState.FIX_FAILED;
|
|
||||||
}
|
|
||||||
result.setState(s);
|
|
||||||
}, Platform::runLater);
|
}, Platform::runLater);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fix(DiagnosticResult diagnosis) {
|
public void fix(DiagnosticResult diagnosis) {
|
||||||
|
Preconditions.checkArgument(diagnosis.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result");
|
||||||
try (var masterkeyClone = masterkey.copy(); //
|
try (var masterkeyClone = masterkey.copy(); //
|
||||||
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
|
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
|
||||||
diagnosis.getFix(vaultPath, vaultConfig, masterkeyClone, cryptor) //
|
diagnosis.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
|
||||||
.orElseThrow(() -> new IllegalStateException("No fix for diagnosis " + diagnosis.getClass().getName() + " implemented.")) //
|
|
||||||
.apply();
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new FixFailedException(e);
|
throw new FixFailedException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class FixFailedException extends CompletionException {
|
public static class FixFailedException extends CompletionException {
|
||||||
|
|
||||||
private FixFailedException(Throwable cause) {
|
private FixFailedException(Throwable cause) {
|
||||||
super(cause);
|
super(cause);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
import javafx.beans.binding.BooleanBinding;
|
||||||
import javafx.beans.binding.ObjectBinding;
|
import javafx.beans.binding.ObjectBinding;
|
||||||
@@ -33,6 +34,7 @@ public class ResultListCellController implements FxController {
|
|||||||
private static final FontAwesome5Icon WARN_ICON = FontAwesome5Icon.EXCLAMATION_TRIANGLE;
|
private static final FontAwesome5Icon WARN_ICON = FontAwesome5Icon.EXCLAMATION_TRIANGLE;
|
||||||
private static final FontAwesome5Icon CRIT_ICON = FontAwesome5Icon.TIMES;
|
private static final FontAwesome5Icon CRIT_ICON = FontAwesome5Icon.TIMES;
|
||||||
|
|
||||||
|
private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class);
|
||||||
|
|
||||||
private final ObjectProperty<Result> result;
|
private final ObjectProperty<Result> result;
|
||||||
private final ObservableValue<DiagnosticResult.Severity> severity;
|
private final ObservableValue<DiagnosticResult.Severity> severity;
|
||||||
@@ -40,17 +42,17 @@ public class ResultListCellController implements FxController {
|
|||||||
private final ResultFixApplier fixApplier;
|
private final ResultFixApplier fixApplier;
|
||||||
private final ObservableValue<Result.FixState> fixState;
|
private final ObservableValue<Result.FixState> fixState;
|
||||||
private final ObjectBinding<FontAwesome5Icon> severityGlyph;
|
private final ObjectBinding<FontAwesome5Icon> severityGlyph;
|
||||||
|
private final ObjectBinding<FontAwesome5Icon> fixGlyph;
|
||||||
private final BooleanBinding fixable;
|
private final BooleanBinding fixable;
|
||||||
private final BooleanBinding fixing;
|
private final BooleanBinding fixing;
|
||||||
private final BooleanBinding fixed;
|
private final BooleanBinding fixed;
|
||||||
private final BooleanBinding fixFailed;
|
private final BooleanBinding fixFailed;
|
||||||
private final BooleanBinding fixRunningOrDone;
|
private final BooleanBinding fixRunningOrDone;
|
||||||
private final ObservableValue<FontAwesome5Icon> fixGlyph;
|
|
||||||
private final List<Subscription> subscriptions;
|
private final List<Subscription> subscriptions;
|
||||||
private final Tooltip fixStateTip;
|
private final Tooltip fixSuccess;
|
||||||
private final Tooltip severityTip;
|
private final Tooltip fixFail;
|
||||||
|
|
||||||
private AutoAnimator fixRunningRotator;
|
private AutoAnimator fixRunningRotator;
|
||||||
private final ResourceBundle resourceBundle;
|
|
||||||
|
|
||||||
/* FXML */
|
/* FXML */
|
||||||
public FontAwesome5IconView severityView;
|
public FontAwesome5IconView severityView;
|
||||||
@@ -58,54 +60,23 @@ public class ResultListCellController implements FxController {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ResultListCellController(ResultFixApplier fixApplier, ResourceBundle resourceBundle) {
|
public ResultListCellController(ResultFixApplier fixApplier, ResourceBundle resourceBundle) {
|
||||||
this.resourceBundle = resourceBundle;
|
|
||||||
this.result = new SimpleObjectProperty<>(null);
|
this.result = new SimpleObjectProperty<>(null);
|
||||||
this.severity = result.map(Result::diagnosis).map(DiagnosticResult::getSeverity);
|
this.severity = result.map(Result::diagnosis).map(DiagnosticResult::getSeverity);
|
||||||
this.description = result.map(Result::getDescription).orElse("");
|
this.description = result.map(Result::getDescription).orElse("");
|
||||||
this.fixApplier = fixApplier;
|
this.fixApplier = fixApplier;
|
||||||
this.fixState = result.flatMap(Result::fixState);
|
this.fixState = result.flatMap(Result::fixState);
|
||||||
this.severityGlyph = Bindings.createObjectBinding(this::getSeverityGlyph, result);
|
this.severityGlyph = Bindings.createObjectBinding(this::getSeverityGlyph, result);
|
||||||
|
this.fixGlyph = Bindings.createObjectBinding(this::getFixGlyph, fixState);
|
||||||
this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState);
|
this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState);
|
||||||
this.fixing = Bindings.createBooleanBinding(this::isFixing, fixState);
|
this.fixing = Bindings.createBooleanBinding(this::isFixing, fixState);
|
||||||
this.fixed = Bindings.createBooleanBinding(this::isFixed, fixState);
|
this.fixed = Bindings.createBooleanBinding(this::isFixed, fixState);
|
||||||
this.fixFailed = Bindings.createBooleanBinding(this::isFixFailed, fixState);
|
this.fixFailed = Bindings.createBooleanBinding(this::isFixFailed, fixState);
|
||||||
this.fixRunningOrDone = fixing.or(fixed).or(fixFailed);
|
this.fixRunningOrDone = fixing.or(fixed).or(fixFailed);
|
||||||
this.fixGlyph = fixState.map(this::getFixGlyph);
|
|
||||||
this.subscriptions = new ArrayList<>();
|
this.subscriptions = new ArrayList<>();
|
||||||
|
this.fixSuccess = new Tooltip(resourceBundle.getString("health.fix.successTip"));
|
||||||
this.fixStateTip = new Tooltip();
|
this.fixFail = new Tooltip(resourceBundle.getString("health.fix.failTip"));
|
||||||
fixStateTip.textProperty().bind(fixState.map(this::getFixStateDescription));
|
fixSuccess.setShowDelay(Duration.millis(100));
|
||||||
fixStateTip.setShowDelay(Duration.millis(100));
|
fixFail.setShowDelay(Duration.millis(100));
|
||||||
|
|
||||||
this.severityTip = new Tooltip();
|
|
||||||
severityTip.textProperty().bind(severity.map(this::getSeverityDescription));
|
|
||||||
severityTip.setShowDelay(Duration.millis(150));
|
|
||||||
}
|
|
||||||
|
|
||||||
public FontAwesome5Icon getFixGlyph(Result.FixState state) {
|
|
||||||
return switch (state) {
|
|
||||||
case FIXING -> FontAwesome5Icon.SPINNER;
|
|
||||||
case FIXED -> FontAwesome5Icon.CHECK;
|
|
||||||
case FIX_FAILED -> FontAwesome5Icon.TIMES;
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getFixStateDescription(Result.FixState fixState) {
|
|
||||||
return switch (fixState) {
|
|
||||||
case FIXED -> resourceBundle.getString("health.fix.successTip");
|
|
||||||
case FIX_FAILED -> resourceBundle.getString("health.fix.failTip");
|
|
||||||
default -> "";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getSeverityDescription(DiagnosticResult.Severity severity) {
|
|
||||||
return resourceBundle.getString(switch (severity) {
|
|
||||||
case GOOD -> "health.result.severityTip.good";
|
|
||||||
case INFO -> "health.result.severityTip.info";
|
|
||||||
case WARN -> "health.result.severityTip.warn";
|
|
||||||
case CRITICAL -> "health.result.severityTip.crit";
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -122,19 +93,22 @@ public class ResultListCellController implements FxController {
|
|||||||
.onCondition(fixing) //
|
.onCondition(fixing) //
|
||||||
.afterStop(() -> fixView.setRotate(0)) //
|
.afterStop(() -> fixView.setRotate(0)) //
|
||||||
.build();
|
.build();
|
||||||
fixState.addListener(((observable, oldValue, newValue) -> {
|
|
||||||
if (newValue == Result.FixState.FIXED || newValue == Result.FixState.FIX_FAILED) {
|
|
||||||
Tooltip.install(fixView, fixStateTip);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
Tooltip.install(severityView, severityTip);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void fix() {
|
public void fix() {
|
||||||
Result r = result.get();
|
Result r = result.get();
|
||||||
if (r != null) {
|
if (r != null) {
|
||||||
fixApplier.fix(r);
|
fixApplier.fix(r).whenCompleteAsync(this::fixFinished, Platform::runLater);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fixFinished(Void unused, Throwable exception) {
|
||||||
|
if (exception != null) {
|
||||||
|
LOG.error("Failed to apply fix", exception);
|
||||||
|
Tooltip.install(fixView, fixFail);
|
||||||
|
} else {
|
||||||
|
Tooltip.install(fixView, fixSuccess);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,12 +152,20 @@ public class ResultListCellController implements FxController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableValue<FontAwesome5Icon> fixGlyphProperty() {
|
public ObjectBinding<FontAwesome5Icon> fixGlyphProperty() {
|
||||||
return fixGlyph;
|
return fixGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FontAwesome5Icon getFixGlyph() {
|
public FontAwesome5Icon getFixGlyph() {
|
||||||
return fixGlyph.getValue();
|
if (fixState.getValue() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return switch (fixState.getValue()) {
|
||||||
|
case NOT_FIXABLE, FIXABLE -> null;
|
||||||
|
case FIXING -> FontAwesome5Icon.SPINNER;
|
||||||
|
case FIXED -> FontAwesome5Icon.CHECK;
|
||||||
|
case FIX_FAILED -> FontAwesome5Icon.TIMES;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanBinding fixableProperty() {
|
public BooleanBinding fixableProperty() {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class LockForcedController implements FxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isForceSupported() {
|
public boolean isForceSupported() {
|
||||||
return vault.supportsForcedUnmount();
|
return false;//vault.supportsForcedUnmount(); TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package org.cryptomator.ui.lock;
|
package org.cryptomator.ui.lock;
|
||||||
|
|
||||||
import dagger.Lazy;
|
import dagger.Lazy;
|
||||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.common.vaults.VaultState;
|
import org.cryptomator.common.vaults.VaultState;
|
||||||
import org.cryptomator.common.vaults.Volume;
|
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||||
import org.cryptomator.ui.common.FxmlFile;
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
import org.cryptomator.ui.common.FxmlScene;
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||||
@@ -17,6 +16,7 @@ import javafx.concurrent.Task;
|
|||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import javafx.stage.Window;
|
import javafx.stage.Window;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
@@ -53,21 +53,21 @@ public class LockWorkflow extends Task<Void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException, ExecutionException {
|
protected Void call() throws InterruptedException, ExecutionException, IOException {
|
||||||
lock(false);
|
lock(false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void lock(boolean forced) throws InterruptedException, ExecutionException {
|
private void lock(boolean forced) throws InterruptedException, ExecutionException, IOException { //TODO: catch or rethrow IOException?
|
||||||
try {
|
try {
|
||||||
vault.lock(forced);
|
vault.lock(forced);
|
||||||
} catch (Volume.VolumeException | LockNotCompletedException e) {
|
} catch (UnmountFailedException e) {
|
||||||
LOG.info("Locking {} failed (forced: {}).", vault.getDisplayName(), forced, e);
|
LOG.info("Locking {} failed (forced: {}).", vault.getDisplayName(), forced, e);
|
||||||
retryOrCancel();
|
retryOrCancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void retryOrCancel() throws ExecutionException, InterruptedException {
|
private void retryOrCancel() throws ExecutionException, InterruptedException, IOException {
|
||||||
try {
|
try {
|
||||||
boolean forced = askWhetherToUseTheForce().get();
|
boolean forced = askWhetherToUseTheForce().get();
|
||||||
lock(forced);
|
lock(forced);
|
||||||
@@ -105,7 +105,7 @@ public class LockWorkflow extends Task<Void> {
|
|||||||
final var throwable = super.getException();
|
final var throwable = super.getException();
|
||||||
LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable);
|
LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable);
|
||||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||||
if (throwable instanceof Volume.VolumeException) {
|
if (throwable instanceof UnmountFailedException) { //TODO: check if correct exception caught
|
||||||
lockWindow.setScene(lockFailedScene.get());
|
lockWindow.setScene(lockFailedScene.get());
|
||||||
lockWindow.show();
|
lockWindow.show();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,16 +4,24 @@ import com.google.common.cache.CacheBuilder;
|
|||||||
import com.google.common.cache.CacheLoader;
|
import com.google.common.cache.CacheLoader;
|
||||||
import com.google.common.cache.LoadingCache;
|
import com.google.common.cache.LoadingCache;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.common.vaults.VaultState;
|
||||||
|
import org.cryptomator.integrations.mount.Mountpoint;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.common.VaultService;
|
import org.cryptomator.ui.common.VaultService;
|
||||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||||
import org.cryptomator.ui.stats.VaultStatisticsComponent;
|
import org.cryptomator.ui.stats.VaultStatisticsComponent;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@MainWindowScoped
|
@MainWindowScoped
|
||||||
public class VaultDetailUnlockedController implements FxController {
|
public class VaultDetailUnlockedController implements FxController {
|
||||||
@@ -24,6 +32,10 @@ public class VaultDetailUnlockedController implements FxController {
|
|||||||
private final Stage mainWindow;
|
private final Stage mainWindow;
|
||||||
private final LoadingCache<Vault, VaultStatisticsComponent> vaultStats;
|
private final LoadingCache<Vault, VaultStatisticsComponent> vaultStats;
|
||||||
private final VaultStatisticsComponent.Builder vaultStatsBuilder;
|
private final VaultStatisticsComponent.Builder vaultStatsBuilder;
|
||||||
|
private final ObservableValue<Mountpoint> mountPoint;
|
||||||
|
private final ObservableValue<Boolean> accessibleViaPath;
|
||||||
|
private final ObservableValue<Boolean> accessibleViaUri;
|
||||||
|
private final ObservableValue<String> mountUri;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
|
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
|
||||||
@@ -33,6 +45,10 @@ public class VaultDetailUnlockedController implements FxController {
|
|||||||
this.mainWindow = mainWindow;
|
this.mainWindow = mainWindow;
|
||||||
this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
|
this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
|
||||||
this.vaultStatsBuilder = vaultStatsBuilder;
|
this.vaultStatsBuilder = vaultStatsBuilder;
|
||||||
|
this.mountPoint = vault.flatMap(Vault::mountPointProperty);
|
||||||
|
this.accessibleViaPath = mountPoint.map(m -> m instanceof Mountpoint.WithPath).orElse(false);
|
||||||
|
this.accessibleViaUri = mountPoint.map(m -> m instanceof Mountpoint.WithUri).orElse(false);
|
||||||
|
this.mountUri = mountPoint.map(Mountpoint::uri).map(URI::toASCIIString).orElse("");
|
||||||
}
|
}
|
||||||
|
|
||||||
private VaultStatisticsComponent buildVaultStats(Vault vault) {
|
private VaultStatisticsComponent buildVaultStats(Vault vault) {
|
||||||
@@ -44,6 +60,11 @@ public class VaultDetailUnlockedController implements FxController {
|
|||||||
vaultService.reveal(vault.get());
|
vaultService.reveal(vault.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void copyMountUri() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void lock() {
|
public void lock() {
|
||||||
appWindows.startLockWorkflow(vault.get(), mainWindow);
|
appWindows.startLockWorkflow(vault.get(), mainWindow);
|
||||||
@@ -64,4 +85,29 @@ public class VaultDetailUnlockedController implements FxController {
|
|||||||
return vault.get();
|
return vault.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObservableValue<Boolean> accessibleViaPathProperty() {
|
||||||
|
return accessibleViaPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAccessibleViaPath() {
|
||||||
|
return accessibleViaPath.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableValue<Boolean> accessibleViaUriProperty() {
|
||||||
|
return accessibleViaUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAccessibleViaUri() {
|
||||||
|
return accessibleViaUri.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableValue<String> mountUriProperty() {
|
||||||
|
return mountUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMountUri() {
|
||||||
|
return mountUri.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
package org.cryptomator.ui.preferences;
|
package org.cryptomator.ui.preferences;
|
||||||
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.settings.Settings;
|
import org.cryptomator.common.settings.Settings;
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
import org.cryptomator.integrations.mount.MountCapability;
|
||||||
import org.cryptomator.common.settings.WebDavUrlScheme;
|
import org.cryptomator.integrations.mount.MountService;
|
||||||
import org.cryptomator.common.vaults.Volume;
|
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
import javafx.beans.binding.BooleanExpression;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ChoiceBox;
|
import javafx.scene.control.ChoiceBox;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: if WebDAV is selected under Windows, show warning that specific mount options (like selecting a directory as mount point) are _not_ supported
|
* TODO: if WebDAV is selected under Windows, show warning that specific mount options (like selecting a directory as mount point) are _not_ supported
|
||||||
@@ -23,40 +22,36 @@ import javafx.util.StringConverter;
|
|||||||
public class VolumePreferencesController implements FxController {
|
public class VolumePreferencesController implements FxController {
|
||||||
|
|
||||||
private final Settings settings;
|
private final Settings settings;
|
||||||
private final BooleanBinding showWebDavSettings;
|
private final ObservableValue<MountService> selectedMountService;
|
||||||
private final BooleanBinding showWebDavScheme;
|
private final BooleanExpression loopbackPortSupported;
|
||||||
public ChoiceBox<VolumeImpl> volumeTypeChoiceBox;
|
private final List<MountService> mountProviders;
|
||||||
public TextField webDavPortField;
|
public ChoiceBox<MountService> volumeTypeChoiceBox;
|
||||||
public Button changeWebDavPortButton;
|
public TextField loopbackPortField;
|
||||||
public ChoiceBox<WebDavUrlScheme> webDavUrlSchemeChoiceBox;
|
public Button loopbackPortApplyButton;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
VolumePreferencesController(Settings settings) {
|
VolumePreferencesController(Settings settings, List<MountService> mountProviders, ObservableValue<MountService> selectedMountService) {
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.showWebDavSettings = Bindings.equal(settings.preferredVolumeImpl(), VolumeImpl.WEBDAV);
|
this.mountProviders = mountProviders;
|
||||||
this.showWebDavScheme = showWebDavSettings.and(new SimpleBooleanProperty(SystemUtils.IS_OS_LINUX)); //TODO: remove SystemUtils
|
this.selectedMountService = selectedMountService;
|
||||||
|
this.loopbackPortSupported = BooleanExpression.booleanExpression(selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
volumeTypeChoiceBox.getItems().addAll(Volume.getCurrentSupportedAdapters());
|
volumeTypeChoiceBox.getItems().addAll(mountProviders);
|
||||||
if (!volumeTypeChoiceBox.getItems().contains(settings.preferredVolumeImpl().get())) {
|
volumeTypeChoiceBox.setConverter(new MountServiceConverter());
|
||||||
settings.preferredVolumeImpl().set(VolumeImpl.WEBDAV);
|
volumeTypeChoiceBox.getSelectionModel().select(selectedMountService.getValue());
|
||||||
}
|
volumeTypeChoiceBox.valueProperty().addListener((observableValue, oldProvide, newProvider) -> settings.mountService().set(newProvider.getClass().getName()));
|
||||||
volumeTypeChoiceBox.valueProperty().bindBidirectional(settings.preferredVolumeImpl());
|
|
||||||
volumeTypeChoiceBox.setConverter(new VolumeImplConverter());
|
|
||||||
|
|
||||||
webDavPortField.setText(String.valueOf(settings.port().get()));
|
loopbackPortField.setText(String.valueOf(settings.port().get()));
|
||||||
changeWebDavPortButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(webDavPortField.textProperty()));
|
loopbackPortApplyButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(loopbackPortField.textProperty()));
|
||||||
changeWebDavPortButton.disableProperty().bind(Bindings.createBooleanBinding(this::validateWebDavPort, webDavPortField.textProperty()).not());
|
loopbackPortApplyButton.disableProperty().bind(Bindings.createBooleanBinding(this::validateLoopbackPort, loopbackPortField.textProperty()).not());
|
||||||
|
|
||||||
webDavUrlSchemeChoiceBox.getItems().addAll(WebDavUrlScheme.values());
|
|
||||||
webDavUrlSchemeChoiceBox.valueProperty().bindBidirectional(settings.preferredGvfsScheme());
|
|
||||||
webDavUrlSchemeChoiceBox.setConverter(new WebDavUrlSchemeConverter());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateWebDavPort() {
|
private boolean validateLoopbackPort() {
|
||||||
try {
|
try {
|
||||||
int port = Integer.parseInt(webDavPortField.getText());
|
int port = Integer.parseInt(loopbackPortField.getText());
|
||||||
return port == 0 // choose port automatically
|
return port == 0 // choose port automatically
|
||||||
|| port >= Settings.MIN_PORT && port <= Settings.MAX_PORT; // port within range
|
|| port >= Settings.MIN_PORT && port <= Settings.MAX_PORT; // port within range
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
@@ -64,54 +59,36 @@ public class VolumePreferencesController implements FxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void doChangeWebDavPort() {
|
public void doChangeLoopbackPort() {
|
||||||
settings.port().set(Integer.parseInt(webDavPortField.getText()));
|
if (validateLoopbackPort()) {
|
||||||
|
settings.port().set(Integer.parseInt(loopbackPortField.getText()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Property Getters */
|
/* Property Getters */
|
||||||
|
|
||||||
public BooleanBinding showWebDavSettingsProperty() {
|
public BooleanExpression loopbackPortSupportedProperty() {
|
||||||
return showWebDavSettings;
|
return loopbackPortSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getShowWebDavSettings() {
|
public boolean isLoopbackPortSupported() {
|
||||||
return showWebDavSettings.get();
|
return loopbackPortSupported.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanBinding showWebDavSchemeProperty() {
|
/* Helpers */
|
||||||
return showWebDavScheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean getShowWebDavScheme() {
|
private static class MountServiceConverter extends StringConverter<MountService> {
|
||||||
return showWebDavScheme.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Helper classes */
|
|
||||||
|
|
||||||
private static class WebDavUrlSchemeConverter extends StringConverter<WebDavUrlScheme> {
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(WebDavUrlScheme scheme) {
|
public String toString(MountService provider) {
|
||||||
return scheme.getDisplayName();
|
return provider== null? "None" : provider.displayName(); //TODO: adjust message
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public WebDavUrlScheme fromString(String string) {
|
public MountService fromString(String string) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class VolumeImplConverter extends StringConverter<VolumeImpl> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString(VolumeImpl impl) {
|
|
||||||
return impl.getDisplayName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public VolumeImpl fromString(String string) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,8 @@ import javax.inject.Inject;
|
|||||||
import javafx.animation.Animation;
|
import javafx.animation.Animation;
|
||||||
import javafx.animation.KeyFrame;
|
import javafx.animation.KeyFrame;
|
||||||
import javafx.animation.Timeline;
|
import javafx.animation.Timeline;
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.binding.DoubleBinding;
|
import javafx.beans.binding.DoubleBinding;
|
||||||
import javafx.beans.binding.LongBinding;
|
import javafx.beans.binding.LongBinding;
|
||||||
import javafx.beans.property.LongProperty;
|
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
@@ -23,7 +21,6 @@ import javafx.scene.chart.XYChart.Series;
|
|||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
|
|
||||||
@VaultStatisticsScoped
|
@VaultStatisticsScoped
|
||||||
public class VaultStatisticsController implements FxController {
|
public class VaultStatisticsController implements FxController {
|
||||||
@@ -35,7 +32,6 @@ public class VaultStatisticsController implements FxController {
|
|||||||
private final VaultStats stats;
|
private final VaultStats stats;
|
||||||
private final Series<Number, Number> readData;
|
private final Series<Number, Number> readData;
|
||||||
private final Series<Number, Number> writeData;
|
private final Series<Number, Number> writeData;
|
||||||
private final Series<Number, Number> accessData;
|
|
||||||
private final Timeline ioAnimation;
|
private final Timeline ioAnimation;
|
||||||
private final LongBinding bpsRead;
|
private final LongBinding bpsRead;
|
||||||
private final LongBinding bpsWritten;
|
private final LongBinding bpsWritten;
|
||||||
@@ -48,20 +44,15 @@ public class VaultStatisticsController implements FxController {
|
|||||||
private final LongBinding totalBytesDecrypted;
|
private final LongBinding totalBytesDecrypted;
|
||||||
private final LongBinding filesRead;
|
private final LongBinding filesRead;
|
||||||
private final LongBinding filesWritten;
|
private final LongBinding filesWritten;
|
||||||
private final LongBinding filesAccessed;
|
|
||||||
private final LongBinding totalFilesAccessed;
|
|
||||||
private final LongBinding bpsEncrypted;
|
private final LongBinding bpsEncrypted;
|
||||||
private final LongBinding bpsDecrypted;
|
private final LongBinding bpsDecrypted;
|
||||||
|
|
||||||
public AreaChart<Number, Number> readChart;
|
public AreaChart<Number, Number> readChart;
|
||||||
public AreaChart<Number, Number> writeChart;
|
public AreaChart<Number, Number> writeChart;
|
||||||
public AreaChart<Number, Number> accessChart;
|
|
||||||
public NumberAxis readChartXAxis;
|
public NumberAxis readChartXAxis;
|
||||||
public NumberAxis readChartYAxis;
|
public NumberAxis readChartYAxis;
|
||||||
public NumberAxis writeChartXAxis;
|
public NumberAxis writeChartXAxis;
|
||||||
public NumberAxis writeChartYAxis;
|
public NumberAxis writeChartYAxis;
|
||||||
public NumberAxis accessChartXAxis;
|
|
||||||
public NumberAxis accessChartYAxis;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public VaultStatisticsController(VaultStatisticsComponent component, @VaultStatisticsWindow Stage window, @VaultStatisticsWindow Vault vault) {
|
public VaultStatisticsController(VaultStatisticsComponent component, @VaultStatisticsWindow Stage window, @VaultStatisticsWindow Vault vault) {
|
||||||
@@ -69,7 +60,6 @@ public class VaultStatisticsController implements FxController {
|
|||||||
this.stats = vault.getStats();
|
this.stats = vault.getStats();
|
||||||
this.readData = new Series<>();
|
this.readData = new Series<>();
|
||||||
this.writeData = new Series<>();
|
this.writeData = new Series<>();
|
||||||
this.accessData = new Series<>();
|
|
||||||
this.bpsRead = WeakBindings.bindLong(stats.bytesPerSecondReadProperty());
|
this.bpsRead = WeakBindings.bindLong(stats.bytesPerSecondReadProperty());
|
||||||
this.bpsWritten = WeakBindings.bindLong(stats.bytesPerSecondWrittenProperty());
|
this.bpsWritten = WeakBindings.bindLong(stats.bytesPerSecondWrittenProperty());
|
||||||
this.cacheHitRate = WeakBindings.bindDouble(stats.cacheHitRateProperty());
|
this.cacheHitRate = WeakBindings.bindDouble(stats.cacheHitRateProperty());
|
||||||
@@ -81,13 +71,11 @@ public class VaultStatisticsController implements FxController {
|
|||||||
this.totalBytesEncrypted = WeakBindings.bindLong(stats.totalBytesEncryptedProperty());
|
this.totalBytesEncrypted = WeakBindings.bindLong(stats.totalBytesEncryptedProperty());
|
||||||
this.filesRead = WeakBindings.bindLong(stats.filesRead());
|
this.filesRead = WeakBindings.bindLong(stats.filesRead());
|
||||||
this.filesWritten = WeakBindings.bindLong(stats.filesWritten());
|
this.filesWritten = WeakBindings.bindLong(stats.filesWritten());
|
||||||
this.filesAccessed = WeakBindings.bindLong(stats.filesAccessed());
|
|
||||||
this.totalFilesAccessed = WeakBindings.bindLong(stats.totalFilesAccessed());
|
|
||||||
this.bpsEncrypted = WeakBindings.bindLong(stats.bytesPerSecondEncryptedProperty());
|
this.bpsEncrypted = WeakBindings.bindLong(stats.bytesPerSecondEncryptedProperty());
|
||||||
this.bpsDecrypted = WeakBindings.bindLong(stats.bytesPerSecondDecryptedProperty());
|
this.bpsDecrypted = WeakBindings.bindLong(stats.bytesPerSecondDecryptedProperty());
|
||||||
|
|
||||||
this.ioAnimation = new Timeline(); //TODO Research better timer
|
this.ioAnimation = new Timeline(); //TODO Research better timer
|
||||||
ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(readData, writeData, accessData)));
|
ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(readData, writeData)));
|
||||||
ioAnimation.setCycleCount(Animation.INDEFINITE);
|
ioAnimation.setCycleCount(Animation.INDEFINITE);
|
||||||
ioAnimation.play();
|
ioAnimation.play();
|
||||||
|
|
||||||
@@ -101,7 +89,6 @@ public class VaultStatisticsController implements FxController {
|
|||||||
public void initialize() {
|
public void initialize() {
|
||||||
readChart.getData().addAll(readData);
|
readChart.getData().addAll(readData);
|
||||||
writeChart.getData().addAll(writeData);
|
writeChart.getData().addAll(writeData);
|
||||||
accessChart.getData().addAll(accessData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
|
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
|
||||||
@@ -109,21 +96,16 @@ public class VaultStatisticsController implements FxController {
|
|||||||
private long step = IO_SAMPLING_STEPS;
|
private long step = IO_SAMPLING_STEPS;
|
||||||
private final Series<Number, Number> decryptedBytesRead;
|
private final Series<Number, Number> decryptedBytesRead;
|
||||||
private final Series<Number, Number> encryptedBytesWrite;
|
private final Series<Number, Number> encryptedBytesWrite;
|
||||||
private final Series<Number, Number> accessedFiles;
|
|
||||||
private final long[] maxBuf = new long[IO_SAMPLING_STEPS];
|
private final long[] maxBuf = new long[IO_SAMPLING_STEPS];
|
||||||
private final long[] maxAccessBuf = new long[IO_SAMPLING_STEPS];
|
|
||||||
|
|
||||||
|
public IoSamplingAnimationHandler(Series<Number, Number> readData, Series<Number, Number> writeData) {
|
||||||
public IoSamplingAnimationHandler(Series<Number, Number> readData, Series<Number, Number> writeData, Series<Number, Number> accessData) {
|
|
||||||
this.decryptedBytesRead = readData;
|
this.decryptedBytesRead = readData;
|
||||||
this.encryptedBytesWrite = writeData;
|
this.encryptedBytesWrite = writeData;
|
||||||
this.accessedFiles = accessData;
|
|
||||||
|
|
||||||
// initialize data once and change value of data points later:
|
// initialize data once and change value of data points later:
|
||||||
for (int i = 0; i < IO_SAMPLING_STEPS; i++) {
|
for (int i = 0; i < IO_SAMPLING_STEPS; i++) {
|
||||||
decryptedBytesRead.getData().add(new Data<>(i, 0));
|
decryptedBytesRead.getData().add(new Data<>(i, 0));
|
||||||
encryptedBytesWrite.getData().add(new Data<>(i, 0));
|
encryptedBytesWrite.getData().add(new Data<>(i, 0));
|
||||||
accessedFiles.getData().add(new Data<>(i, 0));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,22 +114,17 @@ public class VaultStatisticsController implements FxController {
|
|||||||
final long currentStep = step++;
|
final long currentStep = step++;
|
||||||
final long decBytes = stats.bytesPerSecondReadProperty().get();
|
final long decBytes = stats.bytesPerSecondReadProperty().get();
|
||||||
final long encBytes = stats.bytesPerSecondWrittenProperty().get();
|
final long encBytes = stats.bytesPerSecondWrittenProperty().get();
|
||||||
final long accFiles = stats.filesAccessed().get();
|
|
||||||
|
|
||||||
maxBuf[(int) currentStep % IO_SAMPLING_STEPS] = Math.max(decBytes, encBytes);
|
maxBuf[(int) currentStep % IO_SAMPLING_STEPS] = Math.max(decBytes, encBytes);
|
||||||
long allTimeMax = Arrays.stream(maxBuf).max().orElse(0L);
|
long allTimeMax = Arrays.stream(maxBuf).max().orElse(0l);
|
||||||
maxAccessBuf[(int) currentStep % IO_SAMPLING_STEPS] = accFiles;
|
|
||||||
long allTimeMaxAccessedFiles = Arrays.stream(maxAccessBuf).max().orElse(0L);
|
|
||||||
|
|
||||||
// remove oldest value:
|
// remove oldest value:
|
||||||
decryptedBytesRead.getData().remove(0);
|
decryptedBytesRead.getData().remove(0);
|
||||||
encryptedBytesWrite.getData().remove(0);
|
encryptedBytesWrite.getData().remove(0);
|
||||||
accessedFiles.getData().remove(0);
|
|
||||||
|
|
||||||
// add latest value:
|
// add latest value:
|
||||||
decryptedBytesRead.getData().add(new Data<>(currentStep, decBytes));
|
decryptedBytesRead.getData().add(new Data<>(currentStep, decBytes));
|
||||||
encryptedBytesWrite.getData().add(new Data<>(currentStep, encBytes));
|
encryptedBytesWrite.getData().add(new Data<>(currentStep, encBytes));
|
||||||
accessedFiles.getData().add(new Data<>(currentStep, accFiles));
|
|
||||||
|
|
||||||
// adjust ranges:
|
// adjust ranges:
|
||||||
readChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS * 1.0);
|
readChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS * 1.0);
|
||||||
@@ -156,9 +133,6 @@ public class VaultStatisticsController implements FxController {
|
|||||||
writeChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS * 1.0);
|
writeChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS * 1.0);
|
||||||
writeChartXAxis.setUpperBound(currentStep);
|
writeChartXAxis.setUpperBound(currentStep);
|
||||||
writeChartYAxis.setUpperBound(allTimeMax);
|
writeChartYAxis.setUpperBound(allTimeMax);
|
||||||
accessChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS * 1.0);
|
|
||||||
accessChartXAxis.setUpperBound(currentStep);
|
|
||||||
accessChartYAxis.setUpperBound(allTimeMaxAccessedFiles);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,12 +207,4 @@ public class VaultStatisticsController implements FxController {
|
|||||||
public LongBinding filesWrittenProperty() {return filesWritten;}
|
public LongBinding filesWrittenProperty() {return filesWritten;}
|
||||||
|
|
||||||
public long getFilesWritten() {return filesWritten.get();}
|
public long getFilesWritten() {return filesWritten.get();}
|
||||||
|
|
||||||
public LongBinding filesAccessedProperty() {return filesAccessed;}
|
|
||||||
|
|
||||||
public long getFilesAccessed() {return filesAccessed.get();}
|
|
||||||
|
|
||||||
public LongBinding totalFilesAccessedProperty() {return totalFilesAccessed;}
|
|
||||||
|
|
||||||
public long getTotalFilesAccessed() {return totalFilesAccessed.get();}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.cryptomator.ui.traymenu;
|
package org.cryptomator.ui.traymenu;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
import org.cryptomator.integrations.common.CheckAvailability;
|
import org.cryptomator.integrations.common.CheckAvailability;
|
||||||
import org.cryptomator.integrations.common.Priority;
|
import org.cryptomator.integrations.common.Priority;
|
||||||
@@ -20,8 +19,6 @@ import java.awt.PopupMenu;
|
|||||||
import java.awt.SystemTray;
|
import java.awt.SystemTray;
|
||||||
import java.awt.Toolkit;
|
import java.awt.Toolkit;
|
||||||
import java.awt.TrayIcon;
|
import java.awt.TrayIcon;
|
||||||
import java.awt.event.MouseAdapter;
|
|
||||||
import java.awt.event.MouseEvent;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@CheckAvailability
|
@CheckAvailability
|
||||||
@@ -31,7 +28,6 @@ public class AwtTrayMenuController implements TrayMenuController {
|
|||||||
private static final Logger LOG = LoggerFactory.getLogger(AwtTrayMenuController.class);
|
private static final Logger LOG = LoggerFactory.getLogger(AwtTrayMenuController.class);
|
||||||
|
|
||||||
private final PopupMenu menu = new PopupMenu();
|
private final PopupMenu menu = new PopupMenu();
|
||||||
private TrayIcon trayIcon;
|
|
||||||
|
|
||||||
@CheckAvailability
|
@CheckAvailability
|
||||||
public static boolean isAvailable() {
|
public static boolean isAvailable() {
|
||||||
@@ -41,7 +37,7 @@ public class AwtTrayMenuController implements TrayMenuController {
|
|||||||
@Override
|
@Override
|
||||||
public void showTrayIcon(byte[] rawImageData, Runnable defaultAction, String tooltip) throws TrayMenuException {
|
public void showTrayIcon(byte[] rawImageData, Runnable defaultAction, String tooltip) throws TrayMenuException {
|
||||||
var image = Toolkit.getDefaultToolkit().createImage(rawImageData);
|
var image = Toolkit.getDefaultToolkit().createImage(rawImageData);
|
||||||
trayIcon = new TrayIcon(image, tooltip, menu);
|
var trayIcon = new TrayIcon(image, tooltip, menu);
|
||||||
|
|
||||||
trayIcon.setImageAutoSize(true);
|
trayIcon.setImageAutoSize(true);
|
||||||
if (SystemUtils.IS_OS_WINDOWS) {
|
if (SystemUtils.IS_OS_WINDOWS) {
|
||||||
@@ -62,16 +58,9 @@ public class AwtTrayMenuController implements TrayMenuController {
|
|||||||
addChildren(menu, items);
|
addChildren(menu, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBeforeOpenMenu(Runnable runnable) {
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBeforeOpenMenu(Runnable listener) {
|
|
||||||
Preconditions.checkNotNull(this.trayIcon);
|
|
||||||
this.trayIcon.addMouseListener(new MouseAdapter() {
|
|
||||||
@Override
|
|
||||||
public void mouseClicked(MouseEvent e) {
|
|
||||||
listener.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addChildren(Menu menu, List<TrayMenuItem> items) {
|
private void addChildren(Menu menu, List<TrayMenuItem> items) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package org.cryptomator.ui.traymenu;
|
|||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.common.vaults.VaultListManager;
|
|
||||||
import org.cryptomator.integrations.tray.ActionItem;
|
import org.cryptomator.integrations.tray.ActionItem;
|
||||||
import org.cryptomator.integrations.tray.SeparatorItem;
|
import org.cryptomator.integrations.tray.SeparatorItem;
|
||||||
import org.cryptomator.integrations.tray.SubMenuItem;
|
import org.cryptomator.integrations.tray.SubMenuItem;
|
||||||
@@ -64,11 +63,6 @@ public class TrayMenuBuilder {
|
|||||||
|
|
||||||
try (var image = getClass().getResourceAsStream(SystemUtils.IS_OS_MAC_OSX ? TRAY_ICON_MAC : TRAY_ICON)) {
|
try (var image = getClass().getResourceAsStream(SystemUtils.IS_OS_MAC_OSX ? TRAY_ICON_MAC : TRAY_ICON)) {
|
||||||
trayMenu.showTrayIcon(image.readAllBytes(), this::showMainWindow, "Cryptomator");
|
trayMenu.showTrayIcon(image.readAllBytes(), this::showMainWindow, "Cryptomator");
|
||||||
trayMenu.onBeforeOpenMenu(() -> {
|
|
||||||
for (Vault vault : vaults) {
|
|
||||||
VaultListManager.redetermineVaultState(vault);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
rebuildMenu();
|
rebuildMenu();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package org.cryptomator.ui.unlock;
|
package org.cryptomator.ui.unlock;
|
||||||
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.vaults.MountPointRequirement;
|
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
|
||||||
@@ -27,30 +25,4 @@ public class UnlockInvalidMountPointController implements FxController {
|
|||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Getter/Setter */
|
|
||||||
|
|
||||||
public String getMountPoint() {
|
|
||||||
return vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getNotExisting() {
|
|
||||||
return getMountPointRequirement() == MountPointRequirement.EMPTY_MOUNT_POINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getExisting() {
|
|
||||||
return getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getDriveLetterOccupied() {
|
|
||||||
return getMountPointRequirement() == MountPointRequirement.UNUSED_ROOT_DIR;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MountPointRequirement getMountPointRequirement() {
|
|
||||||
var requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!")).getMountPointRequirement();
|
|
||||||
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
|
|
||||||
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
|
|
||||||
assert requirement != MountPointRequirement.UNUSED_ROOT_DIR || SystemUtils.IS_OS_WINDOWS; //Not implemented anywhere, but on Windows
|
|
||||||
|
|
||||||
return requirement;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,10 @@ package org.cryptomator.ui.unlock;
|
|||||||
|
|
||||||
import com.google.common.base.Throwables;
|
import com.google.common.base.Throwables;
|
||||||
import dagger.Lazy;
|
import dagger.Lazy;
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
|
||||||
import org.cryptomator.common.vaults.MountPointRequirement;
|
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.common.vaults.VaultState;
|
import org.cryptomator.common.vaults.VaultState;
|
||||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
|
||||||
import org.cryptomator.cryptolib.api.CryptoException;
|
import org.cryptomator.cryptolib.api.CryptoException;
|
||||||
|
import org.cryptomator.integrations.mount.MountFailedException;
|
||||||
import org.cryptomator.ui.common.FxmlFile;
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
import org.cryptomator.ui.common.FxmlScene;
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
import org.cryptomator.ui.common.VaultService;
|
import org.cryptomator.ui.common.VaultService;
|
||||||
@@ -23,9 +20,6 @@ import javafx.concurrent.Task;
|
|||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.DirectoryNotEmptyException;
|
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
|
||||||
import java.nio.file.NotDirectoryException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A multi-step task that consists of background activities as well as user interaction.
|
* A multi-step task that consists of background activities as well as user interaction.
|
||||||
@@ -57,7 +51,7 @@ public class UnlockWorkflow extends Task<Boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
|
protected Boolean call() throws InterruptedException, IOException, CryptoException, MountFailedException {
|
||||||
try {
|
try {
|
||||||
attemptUnlock();
|
attemptUnlock();
|
||||||
return true;
|
return true;
|
||||||
@@ -67,48 +61,17 @@ public class UnlockWorkflow extends Task<Boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
|
private void attemptUnlock() throws IOException, CryptoException, MountFailedException {
|
||||||
try {
|
try {
|
||||||
keyLoadingStrategy.use(vault::unlock);
|
keyLoadingStrategy.use(vault::unlock);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Throwables.propagateIfPossible(e, IOException.class);
|
Throwables.propagateIfPossible(e, IOException.class);
|
||||||
Throwables.propagateIfPossible(e, VolumeException.class);
|
|
||||||
Throwables.propagateIfPossible(e, InvalidMountPointException.class);
|
|
||||||
Throwables.propagateIfPossible(e, CryptoException.class);
|
Throwables.propagateIfPossible(e, CryptoException.class);
|
||||||
|
Throwables.propagateIfPossible(e, MountFailedException.class);
|
||||||
throw new IllegalStateException("unexpected exception type", e);
|
throw new IllegalStateException("unexpected exception type", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleInvalidMountPoint(InvalidMountPointException impExc) {
|
|
||||||
var requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!", impExc)).getMountPointRequirement();
|
|
||||||
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
|
|
||||||
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
|
|
||||||
assert requirement != MountPointRequirement.UNUSED_ROOT_DIR || SystemUtils.IS_OS_WINDOWS; //Not implemented anywhere, but on Windows
|
|
||||||
|
|
||||||
Throwable cause = impExc.getCause();
|
|
||||||
// TODO: apply https://openjdk.java.net/jeps/8213076 in future JDK versions
|
|
||||||
if (cause instanceof NotDirectoryException) {
|
|
||||||
if (requirement == MountPointRequirement.PARENT_NO_MOUNT_POINT) {
|
|
||||||
LOG.error("Unlock failed. Parent folder is missing: {}", cause.getMessage());
|
|
||||||
} else {
|
|
||||||
LOG.error("Unlock failed. Mountpoint doesn't exist (needs to be a folder): {}", cause.getMessage());
|
|
||||||
}
|
|
||||||
showInvalidMountPointScene();
|
|
||||||
} else if (cause instanceof FileAlreadyExistsException) {
|
|
||||||
if (requirement == MountPointRequirement.UNUSED_ROOT_DIR) {
|
|
||||||
LOG.error("Unlock failed. Drive Letter already in use: {}", cause.getMessage());
|
|
||||||
} else {
|
|
||||||
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
|
|
||||||
}
|
|
||||||
showInvalidMountPointScene();
|
|
||||||
} else if (cause instanceof DirectoryNotEmptyException) {
|
|
||||||
LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());
|
|
||||||
showInvalidMountPointScene();
|
|
||||||
} else {
|
|
||||||
handleGenericError(impExc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showInvalidMountPointScene() {
|
private void showInvalidMountPointScene() {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
window.setScene(invalidMountPointScene.get());
|
window.setScene(invalidMountPointScene.get());
|
||||||
@@ -144,11 +107,7 @@ public class UnlockWorkflow extends Task<Boolean> {
|
|||||||
protected void failed() {
|
protected void failed() {
|
||||||
LOG.info("Unlock of '{}' failed.", vault.getDisplayName());
|
LOG.info("Unlock of '{}' failed.", vault.getDisplayName());
|
||||||
Throwable throwable = super.getException();
|
Throwable throwable = super.getException();
|
||||||
if (throwable instanceof InvalidMountPointException e) {
|
|
||||||
handleInvalidMountPoint(e);
|
|
||||||
} else {
|
|
||||||
handleGenericError(throwable);
|
handleGenericError(throwable);
|
||||||
}
|
|
||||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package org.cryptomator.ui.vaultoptions;
|
package org.cryptomator.ui.vaultoptions;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.Environment;
|
import org.cryptomator.common.Environment;
|
||||||
import org.cryptomator.common.settings.Settings;
|
import org.cryptomator.common.mount.WindowsDriveLetters;
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.common.vaults.WindowsDriveLetters;
|
import org.cryptomator.integrations.mount.MountCapability;
|
||||||
|
import org.cryptomator.integrations.mount.MountService;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.property.StringProperty;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.CheckBox;
|
import javafx.scene.control.CheckBox;
|
||||||
@@ -35,97 +31,110 @@ public class MountOptionsController implements FxController {
|
|||||||
|
|
||||||
private final Stage window;
|
private final Stage window;
|
||||||
private final Vault vault;
|
private final Vault vault;
|
||||||
private final VolumeImpl usedVolumeImpl;
|
|
||||||
private final WindowsDriveLetters windowsDriveLetters;
|
private final WindowsDriveLetters windowsDriveLetters;
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
|
|
||||||
|
private final ObservableValue<Boolean> mountpointDirSupported;
|
||||||
|
private final ObservableValue<Boolean> mountpointDriveLetterSupported;
|
||||||
|
private final ObservableValue<Boolean> readOnlySupported;
|
||||||
|
private final ObservableValue<Boolean> mountFlagsSupported;
|
||||||
|
private final ObservableValue<Path> driveLetter;
|
||||||
|
private final ObservableValue<String> directoryPath;
|
||||||
|
|
||||||
|
|
||||||
|
//-- FXML objects --
|
||||||
public CheckBox readOnlyCheckbox;
|
public CheckBox readOnlyCheckbox;
|
||||||
public CheckBox customMountFlagsCheckbox;
|
public CheckBox customMountFlagsCheckbox;
|
||||||
public TextField mountFlags;
|
public TextField mountFlagsField;
|
||||||
public ToggleGroup mountPoint;
|
public ToggleGroup mountPointToggleGroup;
|
||||||
public RadioButton mountPointAuto;
|
public RadioButton mountPointAutoBtn;
|
||||||
public RadioButton mountPointWinDriveLetter;
|
public RadioButton mountPointDriveLetterBtn;
|
||||||
public RadioButton mountPointCustomDir;
|
public RadioButton mountPointDirBtn;
|
||||||
public ChoiceBox<String> driveLetterSelection;
|
public TextField directoryPathField;
|
||||||
|
public ChoiceBox<Path> driveLetterSelection;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, Settings settings, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, Environment environment) {
|
MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue<MountService> mountService, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, Environment environment) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.usedVolumeImpl = settings.preferredVolumeImpl().get();
|
|
||||||
this.windowsDriveLetters = windowsDriveLetters;
|
this.windowsDriveLetters = windowsDriveLetters;
|
||||||
this.resourceBundle = resourceBundle;
|
this.resourceBundle = resourceBundle;
|
||||||
|
this.mountpointDirSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR) || s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT));
|
||||||
|
this.mountpointDriveLetterSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
|
||||||
|
this.mountFlagsSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS));
|
||||||
|
this.readOnlySupported = mountService.map(s -> s.hasCapability(MountCapability.READ_ONLY));
|
||||||
|
this.driveLetter = vault.getVaultSettings().mountPoint().map(p -> isDriveLetter(p) ? p : null);
|
||||||
|
this.directoryPath = vault.getVaultSettings().mountPoint().map(p -> isDriveLetter(p) ? null : p.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
|
|
||||||
// readonly:
|
// readonly:
|
||||||
readOnlyCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().usesReadOnlyMode());
|
readOnlyCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().usesReadOnlyMode());
|
||||||
//TODO: support this feature on Windows
|
|
||||||
if (usedVolumeImpl == VolumeImpl.FUSE && isOsWindows()) {
|
|
||||||
readOnlyCheckbox.setSelected(false); // to prevent invalid states
|
|
||||||
readOnlyCheckbox.setDisable(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// custom mount flags:
|
// custom mount flags:
|
||||||
mountFlags.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().not());
|
mountFlagsField.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().not());
|
||||||
customMountFlagsCheckbox.setSelected(vault.isHavingCustomMountFlags());
|
customMountFlagsCheckbox.setSelected(vault.isHavingCustomMountFlags());
|
||||||
if (vault.isHavingCustomMountFlags()) {
|
|
||||||
mountFlags.textProperty().bindBidirectional(vault.getVaultSettings().mountFlags());
|
|
||||||
readOnlyCheckbox.setSelected(false); // to prevent invalid states
|
|
||||||
} else {
|
|
||||||
mountFlags.textProperty().bind(vault.defaultMountFlagsProperty());
|
|
||||||
}
|
|
||||||
|
|
||||||
// mount point options:
|
//driveLetter choice box
|
||||||
mountPoint.selectedToggleProperty().addListener(this::toggleMountPoint);
|
driveLetterSelection.getItems().addAll(windowsDriveLetters.getAll());
|
||||||
driveLetterSelection.getItems().addAll(windowsDriveLetters.getAllDriveLetters());
|
|
||||||
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle));
|
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle));
|
||||||
driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get());
|
driveLetterSelection.setOnShowing(event -> driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle))); //TODO: does this work?
|
||||||
|
|
||||||
if (vault.getVaultSettings().useCustomMountPath().get() && vault.getVaultSettings().getCustomMountPath().isPresent()) {
|
//mountPoint toggle group
|
||||||
mountPoint.selectToggle(mountPointCustomDir);
|
var mountPoint = vault.getVaultSettings().getMountPoint();
|
||||||
} else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) {
|
if (mountPoint == null) {
|
||||||
mountPoint.selectToggle(mountPointWinDriveLetter);
|
//prepare and select auto
|
||||||
} else {
|
mountPointToggleGroup.selectToggle(mountPointAutoBtn);
|
||||||
mountPoint.selectToggle(mountPointAuto);
|
} else if (mountPoint.getParent() == null && isDriveLetter(mountPoint)) {
|
||||||
|
//prepare and select drive letter
|
||||||
|
mountPointToggleGroup.selectToggle(mountPointDriveLetterBtn);
|
||||||
|
} else if (driveLetterSelection.getValue() == null) {
|
||||||
|
//prepare and select dir
|
||||||
|
mountPointToggleGroup.selectToggle(mountPointDirBtn);
|
||||||
}
|
}
|
||||||
|
mountPointToggleGroup.selectedToggleProperty().addListener(this::selectedToggleChanged);
|
||||||
vault.getVaultSettings().useCustomMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
|
|
||||||
vault.getVaultSettings().winDriveLetter().bind( //
|
|
||||||
Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) //
|
|
||||||
.then(driveLetterSelection.getSelectionModel().selectedItemProperty()) //
|
|
||||||
.otherwise((String) null) //
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void toggleUseCustomMountFlags() {
|
public void toggleUseCustomMountFlags() {
|
||||||
if (customMountFlagsCheckbox.isSelected()) {
|
if (customMountFlagsCheckbox.isSelected()) {
|
||||||
readOnlyCheckbox.setSelected(false); // to prevent invalid states
|
readOnlyCheckbox.setSelected(false); // to prevent invalid states
|
||||||
mountFlags.textProperty().unbind();
|
mountFlagsField.textProperty().unbind();
|
||||||
vault.setCustomMountFlags(vault.defaultMountFlagsProperty().get());
|
vault.setCustomMountFlags(vault.defaultMountFlagsProperty().getValue());
|
||||||
mountFlags.textProperty().bindBidirectional(vault.getVaultSettings().mountFlags());
|
mountFlagsField.textProperty().bindBidirectional(vault.getVaultSettings().mountFlags());
|
||||||
} else {
|
} else {
|
||||||
mountFlags.textProperty().unbindBidirectional(vault.getVaultSettings().mountFlags());
|
mountFlagsField.textProperty().unbindBidirectional(vault.getVaultSettings().mountFlags());
|
||||||
vault.setCustomMountFlags(null);
|
vault.setCustomMountFlags(null);
|
||||||
mountFlags.textProperty().bind(vault.defaultMountFlagsProperty());
|
mountFlagsField.textProperty().bind(vault.defaultMountFlagsProperty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void chooseCustomMountPoint() {
|
public void chooseCustomMountPoint() {
|
||||||
chooseCustomMountPointOrReset(mountPointCustomDir);
|
try {
|
||||||
|
Path chosenPath = chooseCustomMountPointInternal();
|
||||||
|
vault.getVaultSettings().mountPoint().set(chosenPath);
|
||||||
|
} catch (NoDirSelectedException e) {
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void chooseCustomMountPointOrReset(Toggle previousMountToggle) {
|
/**
|
||||||
|
* Prepares and opens a directory chooser dialog.
|
||||||
|
* This method blocks until the dialog is closed.
|
||||||
|
*
|
||||||
|
* @return the absolute path to the chosen directory
|
||||||
|
* @throws NoDirSelectedException if dialog is closed without choosing a directory
|
||||||
|
*/
|
||||||
|
private Path chooseCustomMountPointInternal() throws NoDirSelectedException {
|
||||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||||
directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle"));
|
directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle"));
|
||||||
try {
|
try {
|
||||||
var initialDir = Path.of(vault.getVaultSettings().getCustomMountPath().orElse(System.getProperty("user.home")));
|
var mp = vault.getVaultSettings().mountPoint().get();
|
||||||
|
var initialDir = mp != null && !isDriveLetter(mp) ? mp : Path.of(System.getProperty("user.home"));
|
||||||
|
|
||||||
if(Files.exists(initialDir)) {
|
if (Files.isDirectory(initialDir)) {
|
||||||
directoryChooser.setInitialDirectory(initialDir.toFile());
|
directoryChooser.setInitialDirectory(initialDir.toFile());
|
||||||
}
|
}
|
||||||
} catch (InvalidPathException e) {
|
} catch (InvalidPathException e) {
|
||||||
@@ -133,73 +142,122 @@ public class MountOptionsController implements FxController {
|
|||||||
}
|
}
|
||||||
File file = directoryChooser.showDialog(window);
|
File file = directoryChooser.showDialog(window);
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
vault.getVaultSettings().customMountPath().set(file.getAbsolutePath());
|
return file.toPath();
|
||||||
} else {
|
} else {
|
||||||
mountPoint.selectToggle(previousMountToggle);
|
throw new NoDirSelectedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleMountPoint(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, Toggle oldValue, Toggle newValue) {
|
private void selectedToggleChanged(ObservableValue<? extends Toggle> observable, Toggle oldToggle, Toggle newToggle) {
|
||||||
if (mountPointCustomDir.equals(newValue) && Strings.isNullOrEmpty(vault.getVaultSettings().customMountPath().get())) {
|
Path mountPointToBe = null;
|
||||||
chooseCustomMountPointOrReset(oldValue);
|
try {
|
||||||
|
//Remark: the mountpoint corresponding to the newToggle must be null, otherwise it would not be new!
|
||||||
|
if (mountPointDriveLetterBtn.equals(newToggle)) {
|
||||||
|
mountPointToBe = driveLetterSelection.getItems().get(0);
|
||||||
|
} else if (mountPointDirBtn.equals(newToggle)) {
|
||||||
|
mountPointToBe = chooseCustomMountPointInternal();
|
||||||
|
}
|
||||||
|
vault.getVaultSettings().mountPoint().set(mountPointToBe);
|
||||||
|
} catch (NoDirSelectedException e) {
|
||||||
|
if (!mountPointDirBtn.equals(oldToggle)) {
|
||||||
|
mountPointToggleGroup.selectToggle(oldToggle);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private boolean isDriveLetter(Path mountPoint) {
|
||||||
* Converts 'C' to "C:" to translate between model and GUI.
|
if (mountPoint != null) {
|
||||||
*/
|
var s = mountPoint.toString();
|
||||||
private static class WinDriveLetterLabelConverter extends StringConverter<String> {
|
return s.length() == 3 && mountPoint.toString().endsWith(":\\");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private final Set<String> occupiedDriveLetters;
|
private static class WinDriveLetterLabelConverter extends StringConverter<Path> {
|
||||||
|
|
||||||
|
private final Set<Path> occupiedDriveLetters;
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
|
|
||||||
WinDriveLetterLabelConverter(WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle) {
|
WinDriveLetterLabelConverter(WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle) {
|
||||||
this.occupiedDriveLetters = windowsDriveLetters.getOccupiedDriveLetters();
|
this.occupiedDriveLetters = windowsDriveLetters.getOccupied();
|
||||||
this.resourceBundle = resourceBundle;
|
this.resourceBundle = resourceBundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(String driveLetter) {
|
public String toString(Path driveLetter) {
|
||||||
if (Strings.isNullOrEmpty(driveLetter)) {
|
if (driveLetter == null) {
|
||||||
return "";
|
return "";
|
||||||
} else if (occupiedDriveLetters.contains(driveLetter)) {
|
} else if (occupiedDriveLetters.contains(driveLetter)) {
|
||||||
return driveLetter + ": (" + resourceBundle.getString("vaultOptions.mount.winDriveLetterOccupied") + ")";
|
return driveLetter.toString().substring(0, 2) + " (" + resourceBundle.getString("vaultOptions.mount.winDriveLetterOccupied") + ")";
|
||||||
} else {
|
} else {
|
||||||
return driveLetter + ":";
|
return driveLetter.toString().substring(0, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String fromString(String string) {
|
public Path fromString(String string) {
|
||||||
throw new UnsupportedOperationException();
|
if (string.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return Path.of(string + "\\");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//@formatter:off
|
||||||
|
private static class NoDirSelectedException extends Exception {}
|
||||||
|
//@formatter:on
|
||||||
|
|
||||||
// Getter & Setter
|
// Getter & Setter
|
||||||
|
|
||||||
public boolean isOsWindows() {
|
public ObservableValue<Boolean> mountFlagsSupportedProperty() {
|
||||||
return SystemUtils.IS_OS_WINDOWS;
|
return mountFlagsSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCustomMountPointSupported() {
|
public boolean isMountFlagsSupported() {
|
||||||
return !(usedVolumeImpl == VolumeImpl.WEBDAV && isOsWindows());
|
return mountFlagsSupported.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableValue<Boolean> mountpointDirSupportedProperty() {
|
||||||
|
return mountpointDirSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMountpointDirSupported() {
|
||||||
|
return mountpointDirSupported.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableValue<Boolean> mountpointDriveLetterSupportedProperty() {
|
||||||
|
return mountpointDriveLetterSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMountpointDriveLetterSupported() {
|
||||||
|
return mountpointDriveLetterSupported.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableValue<Boolean> readOnlySupportedProperty() {
|
||||||
|
return mountpointDriveLetterSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isReadOnlySupported() {
|
public boolean isReadOnlySupported() {
|
||||||
return !(usedVolumeImpl == VolumeImpl.FUSE && isOsWindows());
|
return readOnlySupported.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty customMountPathProperty() {
|
public ObservableValue<Path> driveLetterProperty() {
|
||||||
return vault.getVaultSettings().customMountPath();
|
return driveLetter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCustomMountOptionsSupported() {
|
public Path getDriveLetter() {
|
||||||
return usedVolumeImpl != VolumeImpl.WEBDAV;
|
return driveLetter.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCustomMountPath() {
|
public ObservableValue<String> directoryPathProperty() {
|
||||||
return vault.getVaultSettings().customMountPath().get();
|
return directoryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDirectoryPath() {
|
||||||
|
return directoryPath.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,25 +5,18 @@
|
|||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import org.cryptomator.ui.health.CheckStateIconView?>
|
<?import org.cryptomator.ui.health.CheckStateIconView?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.control.Button?>
|
|
||||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
|
||||||
<?import javafx.scene.layout.Region?>
|
|
||||||
<?import javafx.scene.control.ChoiceBox?>
|
|
||||||
<?import javafx.scene.control.ContextMenu?>
|
|
||||||
<?import javafx.scene.control.MenuItem?>
|
|
||||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||||
xmlns="http://javafx.com/javafx"
|
xmlns="http://javafx.com/javafx"
|
||||||
fx:controller="org.cryptomator.ui.health.CheckDetailController"
|
fx:controller="org.cryptomator.ui.health.CheckDetailController"
|
||||||
spacing="12">
|
spacing="6">
|
||||||
<HBox alignment="CENTER" >
|
<Label fx:id="detailHeader" styleClass="label-extra-large" text="${controller.checkName}" contentDisplay="LEFT">
|
||||||
<VBox spacing="12">
|
|
||||||
<Label fx:id="detailHeader" styleClass="label-extra-large" text="${controller.checkName}" contentDisplay="RIGHT">
|
|
||||||
<graphic>
|
<graphic>
|
||||||
<HBox alignment="CENTER" minWidth="25" maxWidth="25">
|
<HBox alignment="CENTER" minWidth="25" maxWidth="25">
|
||||||
<CheckStateIconView check="${controller.check}" glyphSize="20"/>
|
<CheckStateIconView fx:id="checkStateIconView" check="${controller.check}" glyphSize="20"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
</graphic>
|
</graphic>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Label text="%health.check.detail.checkRunning" visible="${controller.checkRunning}" managed="${controller.checkRunning}"/>
|
<Label text="%health.check.detail.checkRunning" visible="${controller.checkRunning}" managed="${controller.checkRunning}"/>
|
||||||
<Label text="%health.check.detail.checkScheduled" visible="${controller.checkScheduled}" managed="${controller.checkScheduled}"/>
|
<Label text="%health.check.detail.checkScheduled" visible="${controller.checkScheduled}" managed="${controller.checkScheduled}"/>
|
||||||
<Label text="%health.check.detail.checkSkipped" visible="${controller.checkSkipped}" managed="${controller.checkSkipped}"/>
|
<Label text="%health.check.detail.checkSkipped" visible="${controller.checkSkipped}" managed="${controller.checkSkipped}"/>
|
||||||
@@ -31,33 +24,6 @@
|
|||||||
<Label text="%health.check.detail.checkFailed" visible="${controller.checkFailed}" managed="${controller.checkFailed}"/>
|
<Label text="%health.check.detail.checkFailed" visible="${controller.checkFailed}" managed="${controller.checkFailed}"/>
|
||||||
<Label text="%health.check.detail.checkFinished" visible="${controller.checkSucceeded && !controller.warnOrCritsExist}" managed="${controller.checkSucceeded && !controller.warnOrCritsExist}"/>
|
<Label text="%health.check.detail.checkFinished" visible="${controller.checkSucceeded && !controller.warnOrCritsExist}" managed="${controller.checkSucceeded && !controller.warnOrCritsExist}"/>
|
||||||
<Label text="%health.check.detail.checkFinishedAndFound" visible="${controller.checkSucceeded && controller.warnOrCritsExist}" managed="${controller.checkSucceeded && controller.warnOrCritsExist}"/>
|
<Label text="%health.check.detail.checkFinishedAndFound" visible="${controller.checkSucceeded && controller.warnOrCritsExist}" managed="${controller.checkSucceeded && controller.warnOrCritsExist}"/>
|
||||||
</VBox>
|
|
||||||
<Region HBox.hgrow="ALWAYS"/>
|
|
||||||
<Button text="%health.check.detail.fixAllSpecificBtn" contentDisplay="RIGHT" graphicTextGap="3" visible="${controller.checkFinished}" managed="${controller.checkFinished}" disable="${! controller.fixAllInfoResultsPossible}" onAction="#fixAllInfoResults">
|
|
||||||
<graphic>
|
|
||||||
<FontAwesome5IconView glyph="INFO_CIRCLE" glyphSize="12" styleClass="glyph-icon-muted"/>
|
|
||||||
</graphic>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
</HBox>
|
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS" visible="${!controller.checkSkipped}" fixedCellSize="25"/>
|
||||||
<VBox spacing="6" VBox.vgrow="ALWAYS">
|
|
||||||
<HBox alignment="CENTER_LEFT" spacing="6">
|
|
||||||
<Label fx:id="filterLbl" text="%health.check.detail.listFilters.label">
|
|
||||||
<graphic>
|
|
||||||
<FontAwesome5IconView glyph="FUNNEL" glyphSize="${filterLbl.height}" styleClass="glyph-icon-muted"/>
|
|
||||||
</graphic>
|
|
||||||
</Label>
|
|
||||||
<ChoiceBox fx:id="severityChoiceBox" />
|
|
||||||
<ChoiceBox fx:id="fixStateChoiceBox" />
|
|
||||||
</HBox>
|
|
||||||
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS" visible="${!controller.checkSkipped}" fixedCellSize="25">
|
|
||||||
<contextMenu>
|
|
||||||
<ContextMenu>
|
|
||||||
<items>
|
|
||||||
<MenuItem text="%generic.button.copy" onAction="#copyResultDetails"/>
|
|
||||||
</items>
|
|
||||||
</ContextMenu>
|
|
||||||
</contextMenu>
|
|
||||||
</ListView>
|
|
||||||
</VBox>
|
|
||||||
</VBox>
|
</VBox>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||||
xmlns="http://javafx.com/javafx"
|
xmlns="http://javafx.com/javafx"
|
||||||
fx:controller="org.cryptomator.ui.health.CheckListController"
|
fx:controller="org.cryptomator.ui.health.CheckListController"
|
||||||
prefWidth="650"
|
prefWidth="600"
|
||||||
prefHeight="400"
|
prefHeight="400"
|
||||||
spacing="12">
|
spacing="12">
|
||||||
<padding>
|
<padding>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||||
xmlns="http://javafx.com/javafx"
|
xmlns="http://javafx.com/javafx"
|
||||||
fx:controller="org.cryptomator.ui.health.StartController"
|
fx:controller="org.cryptomator.ui.health.StartController"
|
||||||
prefWidth="650"
|
prefWidth="600"
|
||||||
prefHeight="400"
|
prefHeight="400"
|
||||||
spacing="12">
|
spacing="12">
|
||||||
<padding>
|
<padding>
|
||||||
|
|||||||
@@ -20,15 +20,10 @@
|
|||||||
<ChoiceBox fx:id="volumeTypeChoiceBox"/>
|
<ChoiceBox fx:id="volumeTypeChoiceBox"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
||||||
<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.showWebDavSettings}">
|
<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.loopbackPortSupported}">
|
||||||
<Label text="%preferences.volume.webdav.port"/>
|
<Label text="%preferences.volume.tcp.port"/>
|
||||||
<NumericTextField fx:id="webDavPortField"/>
|
<NumericTextField fx:id="loopbackPortField"/>
|
||||||
<Button text="%generic.button.apply" fx:id="changeWebDavPortButton" onAction="#doChangeWebDavPort"/>
|
<Button text="%generic.button.apply" fx:id="loopbackPortApplyButton" onAction="#doChangeLoopbackPort"/>
|
||||||
</HBox>
|
|
||||||
|
|
||||||
<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.showWebDavScheme}">
|
|
||||||
<Label text="%preferences.volume.webdav.scheme"/>
|
|
||||||
<ChoiceBox fx:id="webDavUrlSchemeChoiceBox" maxWidth="Infinity"/>
|
|
||||||
</HBox>
|
</HBox>
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
<?import javafx.scene.layout.StackPane?>
|
<?import javafx.scene.layout.StackPane?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.shape.Arc?>
|
<?import javafx.scene.shape.Arc?>
|
||||||
<?import javafx.scene.shape.Circle?>
|
|
||||||
<HBox xmlns="http://javafx.com/javafx"
|
<HBox xmlns="http://javafx.com/javafx"
|
||||||
xmlns:fx="http://javafx.com/fxml"
|
xmlns:fx="http://javafx.com/fxml"
|
||||||
fx:controller="org.cryptomator.ui.stats.VaultStatisticsController"
|
fx:controller="org.cryptomator.ui.stats.VaultStatisticsController"
|
||||||
@@ -23,9 +22,9 @@
|
|||||||
</padding>
|
</padding>
|
||||||
|
|
||||||
<!-- Caching -->
|
<!-- Caching -->
|
||||||
<StackPane prefWidth="200" prefHeight="200">
|
<VBox prefWidth="200" prefHeight="200">
|
||||||
|
<StackPane>
|
||||||
<Group>
|
<Group>
|
||||||
<Circle centerX="100" centerY="100" radius="100" opacity="0"/> <!-- for alignment. visible=false does not yield the desired result -->
|
|
||||||
<Arc styleClass="cache-arc-background" centerX="100" centerY="100" radiusX="100" radiusY="100" startAngle="225" length="-270"/>
|
<Arc styleClass="cache-arc-background" centerX="100" centerY="100" radiusX="100" radiusY="100" startAngle="225" length="-270"/>
|
||||||
<Arc styleClass="cache-arc-foreground" centerX="100" centerY="100" radiusX="100" radiusY="100" startAngle="225" length="${controller.cacheHitDegrees}"/>
|
<Arc styleClass="cache-arc-foreground" centerX="100" centerY="100" radiusX="100" radiusY="100" startAngle="225" length="${controller.cacheHitDegrees}"/>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -34,6 +33,7 @@
|
|||||||
<Label text="%stats.cacheHitRate"/>
|
<Label text="%stats.cacheHitRate"/>
|
||||||
</VBox>
|
</VBox>
|
||||||
</StackPane>
|
</StackPane>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
<!-- Read -->
|
<!-- Read -->
|
||||||
<VBox prefWidth="300" prefHeight="300" spacing="6" alignment="CENTER">
|
<VBox prefWidth="300" prefHeight="300" spacing="6" alignment="CENTER">
|
||||||
@@ -72,22 +72,4 @@
|
|||||||
<DataLabel byteFormat="%stats.encr.total.data.none" kibFormat="%stats.encr.total.data.kib" mibFormat="%stats.encr.total.data.mib" gibFormat="%stats.encr.total.data.gib" dataInBytes="${controller.totalBytesEncrypted}"/>
|
<DataLabel byteFormat="%stats.encr.total.data.none" kibFormat="%stats.encr.total.data.kib" mibFormat="%stats.encr.total.data.mib" gibFormat="%stats.encr.total.data.gib" dataInBytes="${controller.totalBytesEncrypted}"/>
|
||||||
<FormattedLabel format="%stats.write.accessCount" arg1="${controller.filesWritten}"/>
|
<FormattedLabel format="%stats.write.accessCount" arg1="${controller.filesWritten}"/>
|
||||||
</VBox>
|
</VBox>
|
||||||
<!-- Access -->
|
|
||||||
<VBox prefWidth="300" prefHeight="300" spacing="6" alignment="CENTER">
|
|
||||||
<FormattedLabel styleClass="label-large" format="%stats.access.current" arg1="${controller.filesAccessed}"/>
|
|
||||||
<AreaChart fx:id="accessChart" styleClass="io-stats" createSymbols="false" animated="false">
|
|
||||||
<xAxis>
|
|
||||||
<NumberAxis fx:id="accessChartXAxis" styleClass="io-stats" autoRanging="false" forceZeroInRange="false" side="BOTTOM"/>
|
|
||||||
</xAxis>
|
|
||||||
<yAxis>
|
|
||||||
<NumberAxis fx:id="accessChartYAxis" styleClass="io-stats" autoRanging="true" forceZeroInRange="true" side="LEFT" tickUnit="Infinity"/>
|
|
||||||
</yAxis>
|
|
||||||
<cursor>
|
|
||||||
<Cursor fx:constant="DEFAULT"/>
|
|
||||||
</cursor>
|
|
||||||
</AreaChart>
|
|
||||||
<FormattedLabel format="%stats.access.total" arg1="${controller.totalFilesAccessed}"/>
|
|
||||||
<Label/> <!-- to align with other graphs -->
|
|
||||||
<Label/> <!-- to align with other graphs -->
|
|
||||||
</VBox>
|
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|||||||
@@ -39,9 +39,6 @@
|
|||||||
<Insets bottom="6" top="6"/>
|
<Insets bottom="6" top="6"/>
|
||||||
</padding>
|
</padding>
|
||||||
</Label>
|
</Label>
|
||||||
<FormattedLabel visible="${controller.notExisting}" managed="${controller.notExisting}" format="%unlock.error.invalidMountPoint.notExisting" arg1="${controller.mountPoint}" wrapText="true"/>
|
|
||||||
<FormattedLabel visible="${controller.existing}" managed="${controller.existing}" format="%unlock.error.invalidMountPoint.existing" arg1="${controller.mountPoint}" wrapText="true"/>
|
|
||||||
<FormattedLabel visible="${controller.driveLetterOccupied}" managed="${controller.driveLetterOccupied}" format="%unlock.error.invalidMountPoint.driveLetterOccupied" arg1="${controller.mountPoint}" wrapText="true"/>
|
|
||||||
|
|
||||||
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
|
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
|
||||||
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
|
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
|
||||||
|
|||||||
@@ -12,20 +12,26 @@
|
|||||||
alignment="TOP_CENTER"
|
alignment="TOP_CENTER"
|
||||||
spacing="9">
|
spacing="9">
|
||||||
<Label text="%main.vaultDetail.accessLocation"/>
|
<Label text="%main.vaultDetail.accessLocation"/>
|
||||||
<Button styleClass="button-large" contentDisplay="GRAPHIC_ONLY" minWidth="120" onAction="#revealAccessLocation" defaultButton="${controller.vault.unlocked}">
|
<Button styleClass="button-large" contentDisplay="GRAPHIC_ONLY" minWidth="120" onAction="#revealAccessLocation" defaultButton="${controller.accessibleViaPath}" visible="${controller.accessibleViaPath}" managed="${controller.accessibleViaPath}">
|
||||||
<graphic>
|
<graphic>
|
||||||
<HBox spacing="12" alignment="CENTER">
|
<HBox spacing="12" alignment="CENTER">
|
||||||
<FontAwesome5IconView glyph="HDD" glyphSize="24"/>
|
<FontAwesome5IconView glyph="HDD" glyphSize="24"/>
|
||||||
<VBox spacing="4" alignment="CENTER_LEFT">
|
<VBox spacing="4" alignment="CENTER_LEFT">
|
||||||
<Label text="%main.vaultDetail.revealBtn"/>
|
<Label text="%main.vaultDetail.revealBtn"/>
|
||||||
<Label styleClass="label-extra-small" text="${controller.vault.accessPoint}" textOverrun="CENTER_ELLIPSIS"
|
|
||||||
visible="${controller.vault.accessPointPresent}" managed="${controller.vault.accessPointPresent}"/>
|
|
||||||
</VBox>
|
</VBox>
|
||||||
</HBox>
|
</HBox>
|
||||||
</graphic>
|
</graphic>
|
||||||
<tooltip>
|
</Button>
|
||||||
<Tooltip text="${controller.vault.accessPoint}"/>
|
<Button styleClass="button-large" contentDisplay="GRAPHIC_ONLY" minWidth="120" onAction="#copyMountUri" defaultButton="${controller.accessibleViaUri}" visible="${controller.accessibleViaUri}" managed="${controller.accessibleViaUri}">
|
||||||
</tooltip>
|
<graphic>
|
||||||
|
<HBox spacing="12" alignment="CENTER">
|
||||||
|
<FontAwesome5IconView glyph="LINK" glyphSize="24"/>
|
||||||
|
<VBox spacing="4" alignment="CENTER_LEFT">
|
||||||
|
<Label text="%generic.button.copy"/> <!-- TODO -->
|
||||||
|
<Label styleClass="label-extra-small" text="${controller.mountUri}" textOverrun="CENTER_ELLIPSIS"/>
|
||||||
|
</VBox>
|
||||||
|
</HBox>
|
||||||
|
</graphic>
|
||||||
</Button>
|
</Button>
|
||||||
<Button text="%main.vaultDetail.lockBtn" minWidth="120" onAction="#lock">
|
<Button text="%main.vaultDetail.lockBtn" minWidth="120" onAction="#lock">
|
||||||
<graphic>
|
<graphic>
|
||||||
|
|||||||
@@ -16,41 +16,46 @@
|
|||||||
fx:controller="org.cryptomator.ui.vaultoptions.MountOptionsController"
|
fx:controller="org.cryptomator.ui.vaultoptions.MountOptionsController"
|
||||||
spacing="6">
|
spacing="6">
|
||||||
<fx:define>
|
<fx:define>
|
||||||
<ToggleGroup fx:id="mountPoint"/>
|
<ToggleGroup fx:id="mountPointToggleGroup"/>
|
||||||
</fx:define>
|
</fx:define>
|
||||||
<padding>
|
<padding>
|
||||||
<Insets topRightBottomLeft="12"/>
|
<Insets topRightBottomLeft="12"/>
|
||||||
</padding>
|
</padding>
|
||||||
<children>
|
<children>
|
||||||
<CheckBox fx:id="readOnlyCheckbox" text="%vaultOptions.mount.readonly"/>
|
<Label text="Options depend on the selected volume provider in the general preferences"/>
|
||||||
|
<CheckBox fx:id="readOnlyCheckbox" text="%vaultOptions.mount.readonly" visible="${controller.readOnlySupported}" managed="${controller.readOnlySupported}"/>
|
||||||
|
|
||||||
<CheckBox fx:id="customMountFlagsCheckbox" text="%vaultOptions.mount.customMountFlags" onAction="#toggleUseCustomMountFlags" visible="${controller.customMountOptionsSupported}" managed="${controller.customMountOptionsSupported}"/>
|
<VBox visible="${controller.mountFlagsSupported}" managed="${controller.mountFlagsSupported}">
|
||||||
|
<CheckBox fx:id="customMountFlagsCheckbox" text="%vaultOptions.mount.customMountFlags" onAction="#toggleUseCustomMountFlags"/>
|
||||||
<TextField fx:id="mountFlags" HBox.hgrow="ALWAYS" maxWidth="Infinity">
|
<TextField fx:id="mountFlagsField" HBox.hgrow="ALWAYS" maxWidth="Infinity">
|
||||||
<VBox.margin>
|
<VBox.margin>
|
||||||
<Insets left="24"/>
|
<Insets left="24"/>
|
||||||
</VBox.margin>
|
</VBox.margin>
|
||||||
</TextField>
|
</TextField>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
<Label text="%vaultOptions.mount.mountPoint">
|
<Label text="%vaultOptions.mount.mountPoint">
|
||||||
<VBox.margin>
|
<VBox.margin>
|
||||||
<Insets top="9"/>
|
<Insets top="9"/>
|
||||||
</VBox.margin>
|
</VBox.margin>
|
||||||
</Label>
|
</Label>
|
||||||
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointAuto" text="%vaultOptions.mount.mountPoint.auto"/>
|
|
||||||
<HBox spacing="6" visible="${controller.osWindows}" managed="${controller.osWindows}">
|
<RadioButton toggleGroup="${mountPointToggleGroup}" fx:id="mountPointAutoBtn" text="%vaultOptions.mount.mountPoint.auto"/>
|
||||||
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointWinDriveLetter" text="%vaultOptions.mount.mountPoint.driveLetter"/>
|
|
||||||
<ChoiceBox fx:id="driveLetterSelection" disable="${!mountPointWinDriveLetter.selected}"/>
|
<HBox spacing="6" visible="${controller.mountpointDriveLetterSupported}" managed="${controller.mountpointDriveLetterSupported}">
|
||||||
|
<RadioButton toggleGroup="${mountPointToggleGroup}" fx:id="mountPointDriveLetterBtn" text="%vaultOptions.mount.mountPoint.driveLetter"/>
|
||||||
|
<ChoiceBox fx:id="driveLetterSelection" disable="${!mountPointDriveLetterBtn.selected}" value="${controller.driveLetter}"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
<HBox fx:id="customMountPointRadioBtn" spacing="6" alignment="CENTER_LEFT" visible="${controller.customMountOptionsSupported}" managed="${controller.customMountOptionsSupported}">
|
|
||||||
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointCustomDir" text="%vaultOptions.mount.mountPoint.custom" />
|
<HBox spacing="6" alignment="CENTER_LEFT" visible="${controller.mountpointDirSupported}" managed="${controller.mountpointDirSupported}">
|
||||||
<Button text="%vaultOptions.mount.mountPoint.directoryPickerButton" onAction="#chooseCustomMountPoint" contentDisplay="LEFT" disable="${!mountPointCustomDir.selected}">
|
<RadioButton toggleGroup="${mountPointToggleGroup}" fx:id="mountPointDirBtn" text="%vaultOptions.mount.mountPoint.custom"/>
|
||||||
|
<Button text="%vaultOptions.mount.mountPoint.directoryPickerButton" onAction="#chooseCustomMountPoint" contentDisplay="LEFT" disable="${!mountPointDirBtn.selected}">
|
||||||
<graphic>
|
<graphic>
|
||||||
<FontAwesome5IconView glyph="FOLDER_OPEN" glyphSize="15"/>
|
<FontAwesome5IconView glyph="FOLDER_OPEN" glyphSize="15"/>
|
||||||
</graphic>
|
</graphic>
|
||||||
</Button>
|
</Button>
|
||||||
</HBox>
|
</HBox>
|
||||||
<TextField text="${controller.customMountPath}" visible="${mountPointCustomDir.selected}" maxWidth="Infinity" disable="true" managed="${customMountPointRadioBtn.managed}">
|
<TextField fx:id="directoryPathField" text="${controller.directoryPath}" visible="${mountPointDirBtn.selected}" managed="${mountPointDirBtn.managed}" maxWidth="Infinity" editable="false" >
|
||||||
<VBox.margin>
|
<VBox.margin>
|
||||||
<Insets left="24"/>
|
<Insets left="24"/>
|
||||||
</VBox.margin>
|
</VBox.margin>
|
||||||
|
|||||||
@@ -219,27 +219,7 @@ health.check.detail.checkFinished=The check finished successfully.
|
|||||||
health.check.detail.checkFinishedAndFound=The check finished running. Please review the results.
|
health.check.detail.checkFinishedAndFound=The check finished running. Please review the results.
|
||||||
health.check.detail.checkFailed=The check exited due to an error.
|
health.check.detail.checkFailed=The check exited due to an error.
|
||||||
health.check.detail.checkCancelled=The check was cancelled.
|
health.check.detail.checkCancelled=The check was cancelled.
|
||||||
health.check.detail.listFilters.label=Filter
|
|
||||||
health.check.detail.listFilters.severity=Severity
|
|
||||||
health.check.detail.listFilters.fixState=Fix state
|
|
||||||
health.check.detail.fixAllSpecificBtn=Fix all of type
|
|
||||||
health.check.exportBtn=Export Report
|
health.check.exportBtn=Export Report
|
||||||
## Result view
|
|
||||||
health.result.severityFilter.all=Severity - All
|
|
||||||
health.result.severityFilter.good=Good
|
|
||||||
health.result.severityFilter.info=Info
|
|
||||||
health.result.severityFilter.warn=Warning
|
|
||||||
health.result.severityFilter.crit=Critical
|
|
||||||
health.result.severityTip.good=Severity: Good\nNormal vault structure.
|
|
||||||
health.result.severityTip.info=Severity: Info\nVault structure intact, fix suggested.
|
|
||||||
health.result.severityTip.warn=Severity: Warning\nVault structure corrupted, fix highly advised.
|
|
||||||
health.result.severityTip.crit=Severity: Critical\nVault structure corrupted, data loss determined.
|
|
||||||
health.result.fixStateFilter.all=Fix state - All
|
|
||||||
health.result.fixStateFilter.fixable=Fixable
|
|
||||||
health.result.fixStateFilter.notFixable=Not fixable
|
|
||||||
health.result.fixStateFilter.fixing=Fixing…
|
|
||||||
health.result.fixStateFilter.fixed=Fixed
|
|
||||||
health.result.fixStateFilter.fixFailed=Fix failed
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
health.fix.fixBtn=Fix
|
health.fix.fixBtn=Fix
|
||||||
health.fix.successTip=Fix successful
|
health.fix.successTip=Fix successful
|
||||||
@@ -272,8 +252,7 @@ preferences.interface.showTrayIcon=Show tray icon (requires restart)
|
|||||||
## Volume
|
## Volume
|
||||||
preferences.volume=Virtual Drive
|
preferences.volume=Virtual Drive
|
||||||
preferences.volume.type=Volume Type
|
preferences.volume.type=Volume Type
|
||||||
preferences.volume.webdav.port=WebDAV Port
|
preferences.volume.tcp.port=TCP Port
|
||||||
preferences.volume.webdav.scheme=WebDAV Scheme
|
|
||||||
## Updates
|
## Updates
|
||||||
preferences.updates=Updates
|
preferences.updates=Updates
|
||||||
preferences.updates.currentVersion=Current Version: %s
|
preferences.updates.currentVersion=Current Version: %s
|
||||||
@@ -321,11 +300,6 @@ stats.encr.total.data.mib=Data encrypted: %.1f MiB
|
|||||||
stats.encr.total.data.gib=Data encrypted: %.1f GiB
|
stats.encr.total.data.gib=Data encrypted: %.1f GiB
|
||||||
stats.write.accessCount=Total writes: %d
|
stats.write.accessCount=Total writes: %d
|
||||||
|
|
||||||
## Accesses
|
|
||||||
stats.access.current=Access: %d
|
|
||||||
stats.access.total=Total accesses: %d
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=Close
|
main.closeBtn.tooltip=Close
|
||||||
main.minimizeBtn.tooltip=Minimize
|
main.minimizeBtn.tooltip=Minimize
|
||||||
@@ -408,9 +382,9 @@ vaultOptions.mount.winDriveLetterOccupied=occupied
|
|||||||
vaultOptions.mount.mountPoint=Mount Point
|
vaultOptions.mount.mountPoint=Mount Point
|
||||||
vaultOptions.mount.mountPoint.auto=Automatically pick a suitable location
|
vaultOptions.mount.mountPoint.auto=Automatically pick a suitable location
|
||||||
vaultOptions.mount.mountPoint.driveLetter=Use assigned drive letter
|
vaultOptions.mount.mountPoint.driveLetter=Use assigned drive letter
|
||||||
vaultOptions.mount.mountPoint.custom=Custom path
|
vaultOptions.mount.mountPoint.custom=Use chosen directory
|
||||||
vaultOptions.mount.mountPoint.directoryPickerButton=Choose…
|
vaultOptions.mount.mountPoint.directoryPickerButton=Choose…
|
||||||
vaultOptions.mount.mountPoint.directoryPickerTitle=Pick an empty directory
|
vaultOptions.mount.mountPoint.directoryPickerTitle=Pick a directory
|
||||||
## Master Key
|
## Master Key
|
||||||
vaultOptions.masterkey=Password
|
vaultOptions.masterkey=Password
|
||||||
vaultOptions.masterkey.changePasswordBtn=Change Password
|
vaultOptions.masterkey.changePasswordBtn=Change Password
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ migration.impossible.moreInfo=لا يزال ممكناً فتح المخزن ب
|
|||||||
## Start Failure
|
## Start Failure
|
||||||
## Check Selection
|
## Check Selection
|
||||||
## Detail view
|
## Detail view
|
||||||
## Result view
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
|
|
||||||
# Preferences
|
# Preferences
|
||||||
@@ -201,9 +200,6 @@ stats.decr.total.data.none=تم فك تشفير البيانات:-
|
|||||||
## Write
|
## Write
|
||||||
stats.encr.total.data.none=البيانات المشفرة: -
|
stats.encr.total.data.none=البيانات المشفرة: -
|
||||||
|
|
||||||
## Accesses
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=إغلاق
|
main.closeBtn.tooltip=إغلاق
|
||||||
main.minimizeBtn.tooltip=تصغير
|
main.minimizeBtn.tooltip=تصغير
|
||||||
|
|||||||
@@ -53,12 +53,6 @@ addvaultwizard.new.fileAlreadyExists=Файл або тэчка з такім і
|
|||||||
addvaultwizard.new.locationDoesNotExist=Тэчка ва ўказанай сцежцы не існуе альбо ня можа адчыніцца
|
addvaultwizard.new.locationDoesNotExist=Тэчка ва ўказанай сцежцы не існуе альбо ня можа адчыніцца
|
||||||
addvaultwizard.new.locationIsNotWritable=Для гэтай сцежцы бракуе правоў на запіс
|
addvaultwizard.new.locationIsNotWritable=Для гэтай сцежцы бракуе правоў на запіс
|
||||||
addvaultwizard.new.locationIsOk=Пасуючае месца для тваёй скарбніцы
|
addvaultwizard.new.locationIsOk=Пасуючае месца для тваёй скарбніцы
|
||||||
addvaultwizard.new.invalidName=Некарэктная назва скрабніцы
|
|
||||||
addvaultwizard.new.validName=Карэктная назва скрабніцы
|
|
||||||
addvaultwizard.new.validCharacters.message=Нчзва скарбніцы мусіць утрымліваць наступныя знакі:
|
|
||||||
addvaultwizard.new.validCharacters.chars=Слоўныя знакі, накшталт a, ж або 수
|
|
||||||
addvaultwizard.new.validCharacters.numbers=Лічбы
|
|
||||||
addvaultwizard.new.validCharacters.dashes=Злучок (%s) або падкрэслінік (%s)
|
|
||||||
### Password
|
### Password
|
||||||
addvaultwizard.new.createVaultBtn=Стварыць скарбніцу
|
addvaultwizard.new.createVaultBtn=Стварыць скарбніцу
|
||||||
addvaultwizard.new.generateRecoveryKeyChoice=Бяз гэтага пароля ты ня зможаш атрымаць доступ да сваіх даных. Ці хочаш ты мець ключ аднаўлення на выпадак, калі ты згубіш свой пароль?
|
addvaultwizard.new.generateRecoveryKeyChoice=Бяз гэтага пароля ты ня зможаш атрымаць доступ да сваіх даных. Ці хочаш ты мець ключ аднаўлення на выпадак, калі ты згубіш свой пароль?
|
||||||
@@ -82,30 +76,26 @@ addvault.new.readme.accessLocation.2=Гэта месца дуступу да т
|
|||||||
addvault.new.readme.accessLocation.3=Любы дададзены сюды файл будзе зашыфраваны праз Cryptomator. Ты можаш працаваць тут як са звычайнаю тэчкаю альбо дыскам. Гэта толькі расшыфраваны агляд кантэнту, самі файлы заўжды захоўваюцца зашыфраванымі на тваім цвёрдым дыску.
|
addvault.new.readme.accessLocation.3=Любы дададзены сюды файл будзе зашыфраваны праз Cryptomator. Ты можаш працаваць тут як са звычайнаю тэчкаю альбо дыскам. Гэта толькі расшыфраваны агляд кантэнту, самі файлы заўжды захоўваюцца зашыфраванымі на тваім цвёрдым дыску.
|
||||||
addvault.new.readme.accessLocation.4=Ты можаш выдаліць гэты файл.
|
addvault.new.readme.accessLocation.4=Ты можаш выдаліць гэты файл.
|
||||||
## Existing
|
## Existing
|
||||||
addvaultwizard.existing.instruction=Абяры файл "vault.cryptomator" у існуючай скарбніцы. Калі існуе толькі файл "masterkey.cryptomator", абяры яго.
|
|
||||||
addvaultwizard.existing.chooseBtn=Абраць…
|
addvaultwizard.existing.chooseBtn=Абраць…
|
||||||
addvaultwizard.existing.filePickerTitle=Абяры файл скарбніцы
|
addvaultwizard.existing.filePickerTitle=Абяры файл скарбніцы
|
||||||
addvaultwizard.existing.filePickerMimeDesc=Скрабніца Cryptomator
|
addvaultwizard.existing.filePickerMimeDesc=Скрабніца Cryptomator
|
||||||
## Success
|
## Success
|
||||||
addvaultwizard.success.nextStepsInstructions=Скарбніца "%s" дададзеная.\nТабе трэба разамкнуць гэтую скрабніцу, каб атрымаць доступ да зместу альбо дадаць яго. Таксама ты можаш разамкнуць яе ў любы іншы час.
|
|
||||||
addvaultwizard.success.unlockNow=Разамкнуць зараз
|
addvaultwizard.success.unlockNow=Разамкнуць зараз
|
||||||
|
|
||||||
# Remove Vault
|
# Remove Vault
|
||||||
removeVault.title=Выдаліць "%s"
|
removeVault.title=Выдаліць "%s"
|
||||||
removeVault.message=Ці выдаліць скарбніцу?
|
removeVault.message=Ці выдаліць скарбніцу?
|
||||||
removeVault.description=Гэта прывядзе толькі да таго, што Cryptomator забудзецца на гэтую скрынку. Ты зможаш дадаць яе пазней ізноў. Аніякія зашыфраваныя файлы з тайго жорсткага дыску не выдаляцьмуцца.
|
|
||||||
removeVault.confirmBtn=Выдаліць скарбніцу
|
removeVault.confirmBtn=Выдаліць скарбніцу
|
||||||
|
|
||||||
# Change Password
|
# Change Password
|
||||||
changepassword.title=Змяніць пароль
|
changepassword.title=Змяніць пароль
|
||||||
changepassword.enterOldPassword=Увядзі бягучы пароль ад "%s"
|
changepassword.enterOldPassword=Увядзі бягучы пароль ад "%s"
|
||||||
changepassword.finalConfirmation=Я разумею, што калі забудуся на пароль, то згублю доступ да маіх даных
|
changepassword.finalConfirmation=Я разумею, што калі забудуся на пароль, то згублю доступ да сваех даных
|
||||||
|
|
||||||
# Forget Password
|
# Forget Password
|
||||||
forgetPassword.title=Забыцца на пароль
|
forgetPassword.title=Забыўся на пароль
|
||||||
forgetPassword.message=Ці забыцца на захаваны пароль?
|
forgetPassword.message=Забыўся на захаваны пароль?
|
||||||
forgetPassword.description=Гэта выдаліць захаваны пароль ад гэтай скарбніцы з твайго мэнэджара ключоў.
|
forgetPassword.confirmBtn=Забыўся на пароль
|
||||||
forgetPassword.confirmBtn=Забыцца на пароль
|
|
||||||
|
|
||||||
# Unlock
|
# Unlock
|
||||||
unlock.title=Разамкнуць "%s"
|
unlock.title=Разамкнуць "%s"
|
||||||
@@ -114,72 +104,45 @@ unlock.savePassword=Захоўваць пароль
|
|||||||
unlock.unlockBtn=Адамкнуць
|
unlock.unlockBtn=Адамкнуць
|
||||||
## Select
|
## Select
|
||||||
unlock.chooseMasterkey.message=Файл майстра ключоў ня знойдзены
|
unlock.chooseMasterkey.message=Файл майстра ключоў ня знойдзены
|
||||||
unlock.chooseMasterkey.description=Cryptomator ня змог знайсці файл masterkey у тваёй скарбніцы "%s". Калі ласка, абяры самастойна файл з ключом.
|
|
||||||
unlock.chooseMasterkey.filePickerTitle=Абяры файл masterkey
|
unlock.chooseMasterkey.filePickerTitle=Абяры файл masterkey
|
||||||
unlock.chooseMasterkey.filePickerMimeDesc=Майстар ключоў Cryptomator
|
unlock.chooseMasterkey.filePickerMimeDesc=Майстар ключоў Cryptomator
|
||||||
## Success
|
## Success
|
||||||
unlock.success.message=Паспяховае размыканне
|
unlock.success.message=Паспяховае размыканне
|
||||||
unlock.success.description=Змест скарбніцы "%s" цяпер дасяжны праз ёйны мантажны пункт.
|
|
||||||
unlock.success.rememberChoice=Запомні мой выбар і больш не пытай
|
unlock.success.rememberChoice=Запомні мой выбар і больш не пытай
|
||||||
unlock.success.revealBtn=Паказаць дыск
|
unlock.success.revealBtn=Паказаць дыск
|
||||||
## Failure
|
## Failure
|
||||||
unlock.error.message=Немагчыма разамкнуць скарбніцу
|
unlock.error.message=Немагчыма разамкнуць скарбніцу
|
||||||
### Invalid Mount Point
|
### Invalid Mount Point
|
||||||
unlock.error.invalidMountPoint.notExisting=Пункт мантажу "%s" не з'яўляецца пустой тэчкаю альбо ўвогуле не існуе.
|
|
||||||
unlock.error.invalidMountPoint.existing=Пункт мантажу "%s" ужо існуе альбо адсутнічае бацькоўская тэчка.
|
|
||||||
unlock.error.invalidMountPoint.driveLetterOccupied=Дыскавая літара "%s" ужо выкарыстоўваецца.
|
|
||||||
## Hub
|
## Hub
|
||||||
### Waiting
|
### Waiting
|
||||||
hub.auth.message=Чаканне спраўджання…
|
|
||||||
hub.auth.description=Ты мусіш аўтаматычна перанакіравацца на старонку ўваходу.
|
|
||||||
hub.auth.loginLink=Не перанакіраваўся? Пстрыкні сюды, каб адчыніць старонку.
|
|
||||||
### Receive Key
|
### Receive Key
|
||||||
hub.receive.message=Апрацоўка адказу…
|
|
||||||
hub.receive.description=Cryptomator атрымлівае ды апрацоўвае адказ ад Hub. Калі ласка, пачакай.
|
|
||||||
### Register Device
|
### Register Device
|
||||||
hub.register.message=Патрабуецца назва прылады
|
|
||||||
hub.register.description=Здаецца, што ты ў першы раз увайшла/-оў у Hub з гэтай прылады. Каб ідэнтыфікаваць яе для спраўджання доступу, табе трэба назваць гэтую прыладу.
|
|
||||||
hub.register.nameLabel=Назва прылады
|
|
||||||
hub.register.occupiedMsg=Назва ўжо ўжытая
|
|
||||||
hub.register.registerBtn=Пацвердзіць
|
|
||||||
### Registration Success
|
### Registration Success
|
||||||
hub.registerSuccess.message=Прылада атрымала назву
|
|
||||||
hub.registerSuccess.description=Каб атрымаць доступ да скарбніцы, твая прылада мусіць быць спраўджанай уладальнікам скарбніцы.
|
|
||||||
### Registration Failed
|
### Registration Failed
|
||||||
hub.registerFailed.message=Памылка пры называнні прылады
|
|
||||||
hub.registerFailed.description=Падчас прысваення імя адбылася памылка. Па дэтальную інфармацыю звярніся да пратаколу праграмы.
|
|
||||||
### Unauthorized
|
### Unauthorized
|
||||||
hub.unauthorized.message=Адмова ў доступе
|
|
||||||
hub.unauthorized.description=Тваёй прыладзе ў дадзены момант не дазволена мець доступ да гэтай скрабніцы. Запытайся ўладальніка скрабніцы за дазволам.
|
|
||||||
### License Exceeded
|
### License Exceeded
|
||||||
hub.licenseExceeded.message=Ліцэнзія пратэрмінаваная
|
|
||||||
|
|
||||||
|
|
||||||
# Lock
|
# Lock
|
||||||
## Force
|
## Force
|
||||||
lock.forced.message=Замыканне не атрымалася
|
lock.forced.message=Замыканне не атрымалася
|
||||||
lock.forced.description=Замыканне скарбніцы "%s" было заблакаванае праз аперацыі ў апрацоўцы альбо праз адчыненыя файлы. Ты можаш прымусова замкнуць скарбніцу, але гэта можа прывесці да страты незахаваных даных.
|
|
||||||
lock.forced.retryBtn=Паспрабаваць ізноў
|
lock.forced.retryBtn=Паспрабаваць ізноў
|
||||||
lock.forced.forceBtn=Замкнуць прымусова
|
lock.forced.forceBtn=Замкнуць прымусова
|
||||||
## Failure
|
## Failure
|
||||||
lock.fail.message=Не атрымалася замкнуць скарбніцу
|
lock.fail.message=Не атрымалася замкнуць скарбніцу
|
||||||
lock.fail.description=Скарбніцу "%s" не мажліва замкнуць. Упэўніся, што ты захаваў сваю незахаваную працу дзесьці ў іншым месцы, і што скночыліся ўсе важныя працэсы чытання-пісання. Для замыкання скарбніцы забі працэс Cryptomator.
|
|
||||||
|
|
||||||
# Migration
|
# Migration
|
||||||
migration.title=Абнавіць скарбніцу
|
migration.title=Абнавіць скарбніцу
|
||||||
## Start
|
## Start
|
||||||
migration.start.prompt=Тваю скарбніцу "%s" неабходна абнавіць у новы фармат. Перад тым, як пачаць, запэўніся, што не адбываецца працэсу сінхранавання, які можа ўздзейнічаць на скарбніцу.
|
|
||||||
migration.start.confirm=Так, мая скарбніца цалкам сінхранавана
|
migration.start.confirm=Так, мая скарбніца цалкам сінхранавана
|
||||||
## Run
|
## Run
|
||||||
migration.run.enterPassword=Увядзіце пароль да "%s"
|
migration.run.enterPassword=Увядзіце пароль да "%s"
|
||||||
migration.run.startMigrationBtn=Перамясціць скарбніцу
|
migration.run.startMigrationBtn=Перамясціць скарбніцу
|
||||||
migration.run.progressHint=Гэта можа заняць пэўны час…
|
migration.run.progressHint=Гэта можа заняць пэўны час…
|
||||||
## Success
|
## Success
|
||||||
migration.success.nextStepsInstructions=Міграцыя скарбніцы "%s" прайшла паспяхова.\nЦяпер ты можаш разамкнуць сваю скарбніцу.
|
|
||||||
migration.success.unlockNow=Разамкнуць зараз
|
migration.success.unlockNow=Разамкнуць зараз
|
||||||
## Missing file system capabilities
|
## Missing file system capabilities
|
||||||
migration.error.missingFileSystemCapabilities.title=Файлавая сістэма не падтрымліваецца
|
migration.error.missingFileSystemCapabilities.title=Файлавая сістэма не падтрымліваецца
|
||||||
migration.error.missingFileSystemCapabilities.description=Міграцыя не пачалася, бо твая скарбніца знаходзіцца ў непрыдатнай файлавай сістэме.
|
|
||||||
migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Файлавая сістэма не падтрымлівае доўгія назвы файлаў.
|
migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Файлавая сістэма не падтрымлівае доўгія назвы файлаў.
|
||||||
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Файлавая сістэма не падтрымлівае доўгія сцежкі.
|
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Файлавая сістэма не падтрымлівае доўгія сцежкі.
|
||||||
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Файлавая сістэма не дазваляе чытаць у ёй.
|
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Файлавая сістэма не дазваляе чытаць у ёй.
|
||||||
@@ -189,8 +152,6 @@ migration.impossible.heading=Не мажліва перанесці скарбн
|
|||||||
|
|
||||||
# Health Check
|
# Health Check
|
||||||
## Start
|
## Start
|
||||||
health.title=Тэст на цэласнасць для "%s"
|
|
||||||
health.intro.header=Тэст на цэласнасць
|
|
||||||
health.intro.remarkBackup=Калі даныя пашкоджаныя, дапаможа толькі рэзервовая копія.
|
health.intro.remarkBackup=Калі даныя пашкоджаныя, дапаможа толькі рэзервовая копія.
|
||||||
## Start Failure
|
## Start Failure
|
||||||
health.fail.moreInfo=Падрабязней
|
health.fail.moreInfo=Падрабязней
|
||||||
@@ -199,43 +160,18 @@ health.checkList.selectAllButton=Вылучыць усе элементы
|
|||||||
health.checkList.deselectAllButton=Адрабіць вылучэнне ўсіх элементаў
|
health.checkList.deselectAllButton=Адрабіць вылучэнне ўсіх элементаў
|
||||||
health.check.runBatchBtn=Выканаць вылучаныя элементы
|
health.check.runBatchBtn=Выканаць вылучаныя элементы
|
||||||
## Detail view
|
## Detail view
|
||||||
health.check.detail.checkCancelled=Праверка была скасавана.
|
|
||||||
health.check.exportBtn=Экспартаваць справаздачу
|
|
||||||
## Result view
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
health.fix.fixBtn=Выправіць
|
|
||||||
health.fix.successTip=Паспяхова выпраўлена
|
|
||||||
health.fix.failTip=Няўдалае выпраўленне, глядзі пратакол з дэталямі
|
|
||||||
|
|
||||||
# Preferences
|
# Preferences
|
||||||
preferences.title=Налады
|
preferences.title=Налады
|
||||||
## General
|
## General
|
||||||
preferences.general=Агульныя
|
|
||||||
preferences.general.startHidden=Хаваць акно пры запуску Cryptomator
|
|
||||||
preferences.general.autoCloseVaults=Замыкаць адчыненыя скарбніцы аўтаматычна пры выхадзе з праграмы
|
|
||||||
preferences.general.debugLogging=Уключыць пратакаляванне адладкі
|
|
||||||
preferences.general.debugDirectory=Паказаць файлы пратаколу
|
|
||||||
preferences.general.autoStart=Запускаць Cryptomator падчас запуску сістэмы
|
|
||||||
preferences.general.keychainBackend=Захоўваць паролі праз
|
|
||||||
## Interface
|
## Interface
|
||||||
preferences.interface=Інтэрфэйс
|
|
||||||
preferences.interface.theme=Iнтэрфэйс
|
|
||||||
preferences.interface.theme.automatic=Аўтаматычна
|
|
||||||
preferences.interface.theme.dark=Цёмная
|
preferences.interface.theme.dark=Цёмная
|
||||||
preferences.interface.theme.light=Светлая
|
preferences.interface.theme.light=Светлая
|
||||||
preferences.interface.unlockThemes=Разблакаваць цёмную тэму
|
|
||||||
preferences.interface.language=Мова (спатрэбуецца перазапуск)
|
preferences.interface.language=Мова (спатрэбуецца перазапуск)
|
||||||
preferences.interface.language.auto=Сістэма па змаўчанні
|
|
||||||
preferences.interface.interfaceOrientation=Арыентацыя інтэрфэйсу
|
|
||||||
preferences.interface.interfaceOrientation.ltr=Злева ўправа
|
preferences.interface.interfaceOrientation.ltr=Злева ўправа
|
||||||
preferences.interface.interfaceOrientation.rtl=Справа ўлева
|
preferences.interface.interfaceOrientation.rtl=Справа ўлева
|
||||||
preferences.interface.showMinimizeButton=Паказаць кнопку згортвання
|
|
||||||
preferences.interface.showTrayIcon=Паказваць іконку на інфармацыйнай панэлі (спатрэбіцца перазапуск)
|
|
||||||
## Volume
|
## Volume
|
||||||
preferences.volume=Віртуальны дыск
|
|
||||||
preferences.volume.type=Тып тому
|
|
||||||
preferences.volume.webdav.port=Порт WebDAV
|
|
||||||
preferences.volume.webdav.scheme=Схема WebDAV
|
|
||||||
## Updates
|
## Updates
|
||||||
preferences.updates=Абнаўленні
|
preferences.updates=Абнаўленні
|
||||||
preferences.updates.currentVersion=Бягучая версія: %s
|
preferences.updates.currentVersion=Бягучая версія: %s
|
||||||
@@ -244,55 +180,20 @@ preferences.updates.checkNowBtn=Праверыць зараз
|
|||||||
preferences.updates.updateAvailable=Даступна абнаўленне да версіі %s
|
preferences.updates.updateAvailable=Даступна абнаўленне да версіі %s
|
||||||
## Contribution
|
## Contribution
|
||||||
preferences.contribute=Падтрымай нас
|
preferences.contribute=Падтрымай нас
|
||||||
preferences.contribute.getCertificate=Яшчэ ня маеш такога? Даведайся, як атрымаць.
|
|
||||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||||
|
|
||||||
## About
|
## About
|
||||||
preferences.about=Пра нас
|
preferences.about=Пра нас
|
||||||
|
|
||||||
# Vault Statistics
|
# Vault Statistics
|
||||||
stats.title=Статыстыкі для %s
|
|
||||||
## Read
|
## Read
|
||||||
stats.read.throughput.idle=Чытанне: -
|
|
||||||
stats.read.throughput.kibs=Чытанне: %.2f КіБ/с
|
|
||||||
stats.read.throughput.mibs=Чытанне: %.2f МіБ/с
|
|
||||||
stats.read.total.data.none=Прачытана: -
|
|
||||||
stats.read.total.data.kib=Прачытана: %.1f КіБ
|
|
||||||
stats.read.total.data.mib=Прачытана: %.1f МіБ
|
|
||||||
stats.read.total.data.gib=Прачытана: %.1f ҐіБ
|
|
||||||
stats.decr.total.data.none=Расшыфравана: -
|
|
||||||
stats.decr.total.data.kib=Расшыфравана: %.1f КіБ
|
|
||||||
stats.decr.total.data.mib=Расшыфравана: %.1f МіБ
|
|
||||||
stats.decr.total.data.gib=Расшыфравана: %.1f ҐіБ
|
|
||||||
stats.read.accessCount=Агульная колькасць чытанняў: %d
|
|
||||||
## Write
|
## Write
|
||||||
stats.write.throughput.idle=Чытанне: -
|
|
||||||
stats.write.throughput.kibs=Пісанне: %.2f КіБ/с
|
|
||||||
stats.write.throughput.mibs=Пісанне: %.2f МіБ/с
|
|
||||||
stats.write.total.data.none=Напісана: -
|
|
||||||
stats.write.total.data.kib=Запісана: %.1f КіБ
|
|
||||||
stats.write.total.data.mib=Запісана: %.1f МіБ
|
|
||||||
stats.write.total.data.gib=Запісана: %.1f ҐіБ
|
|
||||||
stats.encr.total.data.none=Зашыфравана: -
|
|
||||||
stats.encr.total.data.kib=Зашыфравана: %.1f КіБ
|
|
||||||
stats.encr.total.data.mib=Зашыфравана: %.1f МіБ
|
|
||||||
stats.encr.total.data.gib=Зашыфравана: %.1f ҐіБ
|
|
||||||
stats.write.accessCount=Агульная колькасць запісаў: %d
|
|
||||||
|
|
||||||
## Accesses
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=Зачыніць
|
main.closeBtn.tooltip=Зачыніць
|
||||||
main.minimizeBtn.tooltip=Згарнуць
|
|
||||||
main.preferencesBtn.tooltip=Налады
|
main.preferencesBtn.tooltip=Налады
|
||||||
main.debugModeEnabled.tooltip=Функццыя дыягназавання выключана
|
|
||||||
main.supporterCertificateMissing.tooltip=Калі ласка, падумай пра ахвяраванне
|
|
||||||
## Drag 'n' Drop
|
## Drag 'n' Drop
|
||||||
main.dropZone.dropVault=Дадаць гэтую скарбніцу
|
|
||||||
main.dropZone.unknownDragboardContent=Калі ты хочаш дадаць скарбніцу, перацягні яе ў гэтае акно
|
|
||||||
## Vault List
|
## Vault List
|
||||||
main.vaultlist.emptyList.onboardingInstruction=Пстрыкні тут, каб дадаць скарбніцу
|
|
||||||
main.vaultlist.contextMenu.remove=Выдаліць…
|
main.vaultlist.contextMenu.remove=Выдаліць…
|
||||||
main.vaultlist.contextMenu.lock=Замкнуць
|
main.vaultlist.contextMenu.lock=Замкнуць
|
||||||
main.vaultlist.contextMenu.unlock=Адамкнуць…
|
main.vaultlist.contextMenu.unlock=Адамкнуць…
|
||||||
@@ -306,98 +207,37 @@ main.vaultlist.addVaultBtn=Дадаць скарбніцу
|
|||||||
main.vaultDetail.lockedStatus=ЗАМКНЁНА
|
main.vaultDetail.lockedStatus=ЗАМКНЁНА
|
||||||
main.vaultDetail.unlockBtn=Адамкнуць…
|
main.vaultDetail.unlockBtn=Адамкнуць…
|
||||||
main.vaultDetail.unlockNowBtn=Разамкнуць зараз
|
main.vaultDetail.unlockNowBtn=Разамкнуць зараз
|
||||||
main.vaultDetail.optionsBtn=Параметры скарбніцы
|
|
||||||
main.vaultDetail.passwordSavedInKeychain=Пароль захаваны
|
|
||||||
### Unlocked
|
### Unlocked
|
||||||
main.vaultDetail.unlockedStatus=РАЗАМКНЁНА
|
main.vaultDetail.unlockedStatus=РАЗАМКНЁНА
|
||||||
main.vaultDetail.accessLocation=Змест тваёй скарбніцы даступны тут:
|
|
||||||
main.vaultDetail.revealBtn=Паказаць дыск
|
main.vaultDetail.revealBtn=Паказаць дыск
|
||||||
main.vaultDetail.lockBtn=Замкнуць
|
main.vaultDetail.lockBtn=Замкнуць
|
||||||
main.vaultDetail.bytesPerSecondRead=Чытанне:
|
|
||||||
main.vaultDetail.bytesPerSecondWritten=Пісанне:
|
|
||||||
main.vaultDetail.throughput.idle=бяздзейны
|
|
||||||
main.vaultDetail.throughput.kbps=%.1f КіБ/с
|
|
||||||
main.vaultDetail.throughput.mbps=%.1f МіБ/с
|
|
||||||
main.vaultDetail.stats=Статыстыка скарбніцы
|
|
||||||
### Missing
|
### Missing
|
||||||
main.vaultDetail.missing.info=Cryptomator ня змог знайсці скарбніцу па гэтай сцежцы.
|
|
||||||
main.vaultDetail.missing.recheck=Пераправерыць
|
|
||||||
main.vaultDetail.missing.remove=Выдаліць са спісу скарбніц…
|
|
||||||
main.vaultDetail.missing.changeLocation=Змяніць месцазнаходжанне скарбніцы…
|
|
||||||
### Needs Migration
|
### Needs Migration
|
||||||
main.vaultDetail.migrateButton=Абнавіць скарбніцу
|
main.vaultDetail.migrateButton=Абнавіць скарбніцу
|
||||||
main.vaultDetail.migratePrompt=Тваю скарбніцу трэба сканвертаваць у новы фармат, перад тым як ты зможаш атрымаць доступ да яе
|
|
||||||
### Error
|
### Error
|
||||||
main.vaultDetail.error.info=Адбылася памылка пры загрузцы скарбніцы з дыску.
|
|
||||||
main.vaultDetail.error.reload=Перазагрузіць
|
|
||||||
main.vaultDetail.error.windowTitle=Памылка загрузкі скарбніцы
|
|
||||||
|
|
||||||
# Wrong File Alert
|
# Wrong File Alert
|
||||||
wrongFileAlert.title=Як зашыфраваць файлы
|
|
||||||
wrongFileAlert.message=Ці спрабаваў ты зашыфраваць гэтыя файлы?
|
|
||||||
wrongFileAlert.instruction.0=Каб зашыфраваць файлы, зрабі наступныя крокі:
|
|
||||||
wrongFileAlert.instruction.1=1. Разамкні свая скарбніцу.
|
|
||||||
wrongFileAlert.instruction.2=2. Пстрыкні па "Паказаць"; каб адчыніць том у тваім файлавым мэнэджары.
|
|
||||||
wrongFileAlert.instruction.3=3. Дадай свае файлы ў гэты том.
|
|
||||||
wrongFileAlert.link=Каб атрымаць больш інфармацыі, наведай
|
|
||||||
|
|
||||||
# Vault Options
|
# Vault Options
|
||||||
## General
|
## General
|
||||||
vaultOptions.general=Агульныя
|
|
||||||
vaultOptions.general.vaultName=Назва скарбніцы
|
vaultOptions.general.vaultName=Назва скарбніцы
|
||||||
vaultOptions.general.autoLock.lockAfterTimePart1=Замыкаць пры бяздзейнасці праз
|
|
||||||
vaultOptions.general.autoLock.lockAfterTimePart2=хвілін(ы)
|
|
||||||
vaultOptions.general.unlockAfterStartup=Размыкаць скрабніцу пры запуску Cryptomator
|
|
||||||
vaultOptions.general.actionAfterUnlock=Пасля паспяховага размыкання
|
|
||||||
vaultOptions.general.actionAfterUnlock.ignore=Нічога не рабі
|
|
||||||
vaultOptions.general.actionAfterUnlock.reveal=Паказаць дыск
|
vaultOptions.general.actionAfterUnlock.reveal=Паказаць дыск
|
||||||
vaultOptions.general.actionAfterUnlock.ask=Запытацца
|
|
||||||
|
|
||||||
## Mount
|
## Mount
|
||||||
vaultOptions.mount=Мантажаванне
|
|
||||||
vaultOptions.mount.readonly=Толькі для чытання
|
|
||||||
vaultOptions.mount.customMountFlags=Карыстальніцкія опцыі мантажавання
|
|
||||||
vaultOptions.mount.winDriveLetterOccupied=занята
|
|
||||||
vaultOptions.mount.mountPoint=Пункт мантажавання
|
|
||||||
vaultOptions.mount.mountPoint.auto=Аўтаматычны выбар пасуючага месцазнаходжання
|
|
||||||
vaultOptions.mount.mountPoint.driveLetter=Назначыць літару для дыску
|
|
||||||
vaultOptions.mount.mountPoint.custom=Уласная сцежка
|
|
||||||
vaultOptions.mount.mountPoint.directoryPickerButton=Абраць…
|
vaultOptions.mount.mountPoint.directoryPickerButton=Абраць…
|
||||||
vaultOptions.mount.mountPoint.directoryPickerTitle=Абяры парожнюю тэчку
|
|
||||||
## Master Key
|
## Master Key
|
||||||
vaultOptions.masterkey=Пароль
|
|
||||||
vaultOptions.masterkey.changePasswordBtn=Змяніць пароль
|
vaultOptions.masterkey.changePasswordBtn=Змяніць пароль
|
||||||
vaultOptions.masterkey.forgetSavedPasswordBtn=Забыцца на захаваны пароль
|
|
||||||
vaultOptions.masterkey.showRecoveryKeyBtn=Паказаць ключ аднаўлення
|
|
||||||
vaultOptions.masterkey.recoverPasswordBtn=Скінуць пароль
|
|
||||||
|
|
||||||
|
|
||||||
# Recovery Key
|
# Recovery Key
|
||||||
## Display Recovery Key
|
## Display Recovery Key
|
||||||
recoveryKey.display.title=Паказаць пароль аднаўлення
|
|
||||||
recoveryKey.create.message=Патрабуецца пароль
|
|
||||||
recoveryKey.create.description=Увядзі пароль ад "%s" каб паказаць ягоны код аднаўлення.
|
|
||||||
recoveryKey.display.description=Наступны код аднаўлення можа быць выкарастаны для аднаўлення доступу да "%s":
|
|
||||||
recoveryKey.display.StorageHints=Захоўвай іх у бяспечным месцы, напрыклад:\n • Выкарыстоўваючы мэнэджар пароляў\n • На USB-флэшцы\n • Раздрукаванымі на паперы
|
recoveryKey.display.StorageHints=Захоўвай іх у бяспечным месцы, напрыклад:\n • Выкарыстоўваючы мэнэджар пароляў\n • На USB-флэшцы\n • Раздрукаванымі на паперы
|
||||||
## Reset Password
|
## Reset Password
|
||||||
### Enter Recovery Key
|
### Enter Recovery Key
|
||||||
recoveryKey.recover.title=Скінуць пароль
|
|
||||||
recoveryKey.recover.prompt=Увядзі свой ключ аднаўлення для "%s":
|
|
||||||
recoveryKey.recover.validKey=Гэта валідны ключ аднаўлення
|
|
||||||
recoveryKey.printout.heading=Ключ аднаўлення Cryptomator\n"%s"\n
|
|
||||||
### Reset Password
|
### Reset Password
|
||||||
recoveryKey.recover.resetBtn=Скінуць
|
|
||||||
### Recovery Key Password Reset Success
|
### Recovery Key Password Reset Success
|
||||||
recoveryKey.recover.resetSuccess.message=Пароль паспяхова скінуты
|
|
||||||
recoveryKey.recover.resetSuccess.description=Ты можаш разамкнуць сваю скарбніцу з дапамогаю новага паролю.
|
|
||||||
|
|
||||||
# New Password
|
# New Password
|
||||||
newPassword.promptText=Увядзі новы пароль
|
|
||||||
newPassword.reenterPassword=Пацвердзі новы пароль
|
|
||||||
newPassword.passwordsMatch=Паролі супадаюць!
|
|
||||||
newPassword.passwordsDoNotMatch=Паролі не супадаюць
|
|
||||||
passwordStrength.messageLabel.tooShort=Выкарыстай ня менш за %d сімвалаў
|
|
||||||
passwordStrength.messageLabel.0=Вельмі слабы
|
|
||||||
passwordStrength.messageLabel.1=Слабы
|
passwordStrength.messageLabel.1=Слабы
|
||||||
passwordStrength.messageLabel.2=Нармалёвы
|
passwordStrength.messageLabel.2=Нармалёвы
|
||||||
passwordStrength.messageLabel.3=Моцны
|
passwordStrength.messageLabel.3=Моцны
|
||||||
@@ -406,9 +246,5 @@ passwordStrength.messageLabel.4=Вельмі моцны
|
|||||||
# Quit
|
# Quit
|
||||||
quit.title=Пакінуць праграму
|
quit.title=Пакінуць праграму
|
||||||
quit.message=Існуюць разамкнёныя скарбніцы
|
quit.message=Існуюць разамкнёныя скарбніцы
|
||||||
quit.description=Калі ласка, пацвердзі, што ты збіраешся выйсці. Cryptomator замкне ўсе разомкнутыя скарбніцы, каб прадухіліць страту даных.
|
|
||||||
quit.lockAndQuitBtn=Замкнуць ды вайсці
|
|
||||||
|
|
||||||
# Forced Quit
|
# Forced Quit
|
||||||
quit.forced.message=Некаторыя скарбніцы не магчыма замкнуць
|
|
||||||
quit.forced.forceAndQuitBtn=Прымусіць і выйсці
|
|
||||||
@@ -108,7 +108,6 @@ lock.forced.retryBtn=পুনরায় চেষ্টা করুন
|
|||||||
## Start Failure
|
## Start Failure
|
||||||
## Check Selection
|
## Check Selection
|
||||||
## Detail view
|
## Detail view
|
||||||
## Result view
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
|
|
||||||
# Preferences
|
# Preferences
|
||||||
@@ -125,9 +124,6 @@ lock.forced.retryBtn=পুনরায় চেষ্টা করুন
|
|||||||
## Read
|
## Read
|
||||||
## Write
|
## Write
|
||||||
|
|
||||||
## Accesses
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=বন্ধ করুন
|
main.closeBtn.tooltip=বন্ধ করুন
|
||||||
## Drag 'n' Drop
|
## Drag 'n' Drop
|
||||||
|
|||||||
@@ -154,7 +154,6 @@ migration.impossible.moreInfo=Sef se i dalje može otvoriti sa starijom verzijom
|
|||||||
## Start Failure
|
## Start Failure
|
||||||
## Check Selection
|
## Check Selection
|
||||||
## Detail view
|
## Detail view
|
||||||
## Result view
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
|
|
||||||
# Preferences
|
# Preferences
|
||||||
@@ -216,9 +215,6 @@ stats.encr.total.data.mib=Otključano podataka: %.1f MiB
|
|||||||
stats.encr.total.data.gib=Otključano podataka: %.1f GiB
|
stats.encr.total.data.gib=Otključano podataka: %.1f GiB
|
||||||
stats.write.accessCount=Ukupno upisano: %d
|
stats.write.accessCount=Ukupno upisano: %d
|
||||||
|
|
||||||
## Accesses
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=Zatvori
|
main.closeBtn.tooltip=Zatvori
|
||||||
main.minimizeBtn.tooltip=Minimiziraj
|
main.minimizeBtn.tooltip=Minimiziraj
|
||||||
|
|||||||
@@ -210,23 +210,7 @@ health.check.detail.checkFinished=La prova ha finalitzat amb èxit.
|
|||||||
health.check.detail.checkFinishedAndFound=La comprovació ha finalitzat. Si us plau, comproveu-ne es resultat.
|
health.check.detail.checkFinishedAndFound=La comprovació ha finalitzat. Si us plau, comproveu-ne es resultat.
|
||||||
health.check.detail.checkFailed=La comprovació ha acabat a causa d'un error.
|
health.check.detail.checkFailed=La comprovació ha acabat a causa d'un error.
|
||||||
health.check.detail.checkCancelled=S'ha cancel·lat la prova.
|
health.check.detail.checkCancelled=S'ha cancel·lat la prova.
|
||||||
health.check.detail.listFilters.label=Filtre
|
|
||||||
health.check.detail.listFilters.severity=Gravetat
|
|
||||||
health.check.detail.listFilters.fixState=Estat de la reparació
|
|
||||||
health.check.detail.fixAllSpecificBtn=Qualsevol reparació
|
|
||||||
health.check.exportBtn=Exporta informe
|
health.check.exportBtn=Exporta informe
|
||||||
## Result view
|
|
||||||
health.result.severityFilter.all=Gravetat - Totes
|
|
||||||
health.result.severityFilter.good=Bé
|
|
||||||
health.result.severityFilter.info=Info
|
|
||||||
health.result.severityFilter.warn=Avís
|
|
||||||
health.result.severityFilter.crit=Crític
|
|
||||||
health.result.fixStateFilter.all=Estat de reparació - Tot
|
|
||||||
health.result.fixStateFilter.fixable=Es pot arreglar
|
|
||||||
health.result.fixStateFilter.notFixable=No es pot arreglar
|
|
||||||
health.result.fixStateFilter.fixing=Reparant…
|
|
||||||
health.result.fixStateFilter.fixed=Solucionat
|
|
||||||
health.result.fixStateFilter.fixFailed=Reparació fallida
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
health.fix.fixBtn=Corregeix
|
health.fix.fixBtn=Corregeix
|
||||||
health.fix.successTip=S'ha corregit amb èxit
|
health.fix.successTip=S'ha corregit amb èxit
|
||||||
@@ -308,11 +292,6 @@ stats.encr.total.data.mib=Dades xifrades: %.1f MiB
|
|||||||
stats.encr.total.data.gib=Dades xifrades: %.1f GiB
|
stats.encr.total.data.gib=Dades xifrades: %.1f GiB
|
||||||
stats.write.accessCount=Total escrits: %d
|
stats.write.accessCount=Total escrits: %d
|
||||||
|
|
||||||
## Accesses
|
|
||||||
stats.access.current=Accés: %d
|
|
||||||
stats.access.total=Total d'accessos: %d
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=Tanca
|
main.closeBtn.tooltip=Tanca
|
||||||
main.minimizeBtn.tooltip=Minimitza
|
main.minimizeBtn.tooltip=Minimitza
|
||||||
|
|||||||
@@ -192,7 +192,6 @@ health.check.detail.checkFinishedAndFound=Kontrola byla dokončena. Zkontrolujte
|
|||||||
health.check.detail.checkFailed=Kontrola byla ukončena z důvodu chyby.
|
health.check.detail.checkFailed=Kontrola byla ukončena z důvodu chyby.
|
||||||
health.check.detail.checkCancelled=Kontrola byla zrušena.
|
health.check.detail.checkCancelled=Kontrola byla zrušena.
|
||||||
health.check.exportBtn=Exportovat sestavu
|
health.check.exportBtn=Exportovat sestavu
|
||||||
## Result view
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
health.fix.fixBtn=Opravit
|
health.fix.fixBtn=Opravit
|
||||||
health.fix.successTip=Oprava byla úspěšná
|
health.fix.successTip=Oprava byla úspěšná
|
||||||
@@ -273,9 +272,6 @@ stats.encr.total.data.mib=Zašifrováno: %.1f MiB
|
|||||||
stats.encr.total.data.gib=Zašifrováno: %.1f GiB
|
stats.encr.total.data.gib=Zašifrováno: %.1f GiB
|
||||||
stats.write.accessCount=Celkem zapsáno: %d
|
stats.write.accessCount=Celkem zapsáno: %d
|
||||||
|
|
||||||
## Accesses
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=Zavřít
|
main.closeBtn.tooltip=Zavřít
|
||||||
main.minimizeBtn.tooltip=Minimalizovat
|
main.minimizeBtn.tooltip=Minimalizovat
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ health.check.detail.checkFinishedAndFound=Kontrolproceduren er kørt færdig. Ge
|
|||||||
health.check.detail.checkFailed=Kontrolproceduren blev afbrudt af en fejl.
|
health.check.detail.checkFailed=Kontrolproceduren blev afbrudt af en fejl.
|
||||||
health.check.detail.checkCancelled=Kontrolproceduren blev annulleret.
|
health.check.detail.checkCancelled=Kontrolproceduren blev annulleret.
|
||||||
health.check.exportBtn=Eksportér rapport
|
health.check.exportBtn=Eksportér rapport
|
||||||
## Result view
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
health.fix.fixBtn=Reparér
|
health.fix.fixBtn=Reparér
|
||||||
health.fix.successTip=Repareret
|
health.fix.successTip=Repareret
|
||||||
@@ -301,9 +300,6 @@ stats.encr.total.data.mib=Data krypteret: %.1f MiB
|
|||||||
stats.encr.total.data.gib=Data krypteret: %.1f GiB
|
stats.encr.total.data.gib=Data krypteret: %.1f GiB
|
||||||
stats.write.accessCount=Totalt antal skrivninger: %d
|
stats.write.accessCount=Totalt antal skrivninger: %d
|
||||||
|
|
||||||
## Accesses
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=Luk
|
main.closeBtn.tooltip=Luk
|
||||||
main.minimizeBtn.tooltip=Minimér
|
main.minimizeBtn.tooltip=Minimér
|
||||||
|
|||||||
@@ -218,26 +218,7 @@ health.check.detail.checkFinished=Die Prüfung wurde erfolgreich abgeschlossen.
|
|||||||
health.check.detail.checkFinishedAndFound=Die Prüfung wurde abgeschlossen. Bitte überprüfe die Ergebnisse.
|
health.check.detail.checkFinishedAndFound=Die Prüfung wurde abgeschlossen. Bitte überprüfe die Ergebnisse.
|
||||||
health.check.detail.checkFailed=Die Prüfung wurde wegen eines Fehlers abgebrochen.
|
health.check.detail.checkFailed=Die Prüfung wurde wegen eines Fehlers abgebrochen.
|
||||||
health.check.detail.checkCancelled=Die Prüfung wurde abgebrochen.
|
health.check.detail.checkCancelled=Die Prüfung wurde abgebrochen.
|
||||||
health.check.detail.listFilters.label=Filter
|
|
||||||
health.check.detail.listFilters.severity=Schweregrad
|
|
||||||
health.check.detail.listFilters.fixState=Fix Status
|
|
||||||
health.check.detail.fixAllSpecificBtn=Behebe alle mit Status
|
|
||||||
health.check.exportBtn=Bericht exportieren
|
health.check.exportBtn=Bericht exportieren
|
||||||
## Result view
|
|
||||||
health.result.severityFilter.all=Schweregrad - Alle
|
|
||||||
health.result.severityFilter.good=Gut
|
|
||||||
health.result.severityFilter.info=Info
|
|
||||||
health.result.severityFilter.warn=Warnung
|
|
||||||
health.result.severityFilter.crit=Kritisch
|
|
||||||
health.result.severityTip.good=Schweregrad: Gut\nNormale Tresorstruktur.
|
|
||||||
health.result.severityTip.info=Schweregrad: Info\nTresorstruktur intakt, Beheben empfohlen.
|
|
||||||
health.result.severityTip.warn=Schweregrad: Warnung\nTresorstruktur beschädigt, Beheben dringend empfohlen.
|
|
||||||
health.result.severityTip.crit=Schweregrad: Kritisch\nTresorstruktur beschädigt, Datenverlust möglich.
|
|
||||||
health.result.fixStateFilter.all=Fix-Status - Alle
|
|
||||||
health.result.fixStateFilter.fixable=Fixierbar
|
|
||||||
health.result.fixStateFilter.notFixable=Nicht behebbar
|
|
||||||
health.result.fixStateFilter.fixed=Behoben
|
|
||||||
health.result.fixStateFilter.fixFailed=Fix fehlgeschlagen
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
health.fix.fixBtn=Beheben
|
health.fix.fixBtn=Beheben
|
||||||
health.fix.successTip=Fehlerbehebung erfolgreich
|
health.fix.successTip=Fehlerbehebung erfolgreich
|
||||||
@@ -319,11 +300,6 @@ stats.encr.total.data.mib=Verschlüsselt: %.1f MiB
|
|||||||
stats.encr.total.data.gib=Verschlüsselt: %.1f GiB
|
stats.encr.total.data.gib=Verschlüsselt: %.1f GiB
|
||||||
stats.write.accessCount=Schreibzugriffe: %d
|
stats.write.accessCount=Schreibzugriffe: %d
|
||||||
|
|
||||||
## Accesses
|
|
||||||
stats.access.current=Zugriffe: %d
|
|
||||||
stats.access.total=Gesamte Zugriffe: %d
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=Schließen
|
main.closeBtn.tooltip=Schließen
|
||||||
main.minimizeBtn.tooltip=Minimieren
|
main.minimizeBtn.tooltip=Minimieren
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ health.check.detail.checkFinishedAndFound=Ο έλεγχος σταμάτησε
|
|||||||
health.check.detail.checkFailed=Ο έλεγχος τερματίστηκε λόγω σφάλματος.
|
health.check.detail.checkFailed=Ο έλεγχος τερματίστηκε λόγω σφάλματος.
|
||||||
health.check.detail.checkCancelled=Ο έλεγχος ακυρώθηκε.
|
health.check.detail.checkCancelled=Ο έλεγχος ακυρώθηκε.
|
||||||
health.check.exportBtn=Εξαγωγή Αναφοράς
|
health.check.exportBtn=Εξαγωγή Αναφοράς
|
||||||
## Result view
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
health.fix.fixBtn=Επιδιόρθωση
|
health.fix.fixBtn=Επιδιόρθωση
|
||||||
health.fix.successTip=Επιτυχής επιδιόρθωση
|
health.fix.successTip=Επιτυχής επιδιόρθωση
|
||||||
@@ -301,9 +300,6 @@ stats.encr.total.data.mib=Δεδομένα που κρυπτογραφήθηκα
|
|||||||
stats.encr.total.data.gib=Δεδομένα που κρυπτογραφήθηκαν: %.1f GiB
|
stats.encr.total.data.gib=Δεδομένα που κρυπτογραφήθηκαν: %.1f GiB
|
||||||
stats.write.accessCount=Συνολικές εγγραφές: %d
|
stats.write.accessCount=Συνολικές εγγραφές: %d
|
||||||
|
|
||||||
## Accesses
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=Κλείσιμο
|
main.closeBtn.tooltip=Κλείσιμο
|
||||||
main.minimizeBtn.tooltip=Ελαχιστοποίηση
|
main.minimizeBtn.tooltip=Ελαχιστοποίηση
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ health.check.detail.checkFinishedAndFound=La comprobación terminó de ejecutars
|
|||||||
health.check.detail.checkFailed=La comprobación terminó debido a un error.
|
health.check.detail.checkFailed=La comprobación terminó debido a un error.
|
||||||
health.check.detail.checkCancelled=La comprobación se canceló.
|
health.check.detail.checkCancelled=La comprobación se canceló.
|
||||||
health.check.exportBtn=Exportar informe
|
health.check.exportBtn=Exportar informe
|
||||||
## Result view
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
health.fix.fixBtn=Reparar
|
health.fix.fixBtn=Reparar
|
||||||
health.fix.successTip=Reparación exitosa
|
health.fix.successTip=Reparación exitosa
|
||||||
@@ -301,9 +300,6 @@ stats.encr.total.data.mib=Datos cifrados: %.1f MiB
|
|||||||
stats.encr.total.data.gib=Datos cifrados: %.1f GiB
|
stats.encr.total.data.gib=Datos cifrados: %.1f GiB
|
||||||
stats.write.accessCount=Total de escrituras: %d
|
stats.write.accessCount=Total de escrituras: %d
|
||||||
|
|
||||||
## Accesses
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=Cerrar
|
main.closeBtn.tooltip=Cerrar
|
||||||
main.minimizeBtn.tooltip=Minimizar
|
main.minimizeBtn.tooltip=Minimizar
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ unlock.unlockBtn=بازکردن قفل
|
|||||||
## Start Failure
|
## Start Failure
|
||||||
## Check Selection
|
## Check Selection
|
||||||
## Detail view
|
## Detail view
|
||||||
## Result view
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
|
|
||||||
# Preferences
|
# Preferences
|
||||||
@@ -86,9 +85,6 @@ unlock.unlockBtn=بازکردن قفل
|
|||||||
## Read
|
## Read
|
||||||
## Write
|
## Write
|
||||||
|
|
||||||
## Accesses
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.supporterCertificateMissing.tooltip=لطفا کمک مالی در نظر بگیرند
|
main.supporterCertificateMissing.tooltip=لطفا کمک مالی در نظر بگیرند
|
||||||
## Drag 'n' Drop
|
## Drag 'n' Drop
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ lock.forced.retryBtn=Subukan muli
|
|||||||
## Start Failure
|
## Start Failure
|
||||||
## Check Selection
|
## Check Selection
|
||||||
## Detail view
|
## Detail view
|
||||||
## Result view
|
|
||||||
## Fix Application
|
## Fix Application
|
||||||
|
|
||||||
# Preferences
|
# Preferences
|
||||||
@@ -114,9 +113,6 @@ preferences.interface.theme.light=Light
|
|||||||
## Read
|
## Read
|
||||||
## Write
|
## Write
|
||||||
|
|
||||||
## Accesses
|
|
||||||
|
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
main.closeBtn.tooltip=Isara
|
main.closeBtn.tooltip=Isara
|
||||||
main.preferencesBtn.tooltip=Mga Kagustuhan
|
main.preferencesBtn.tooltip=Mga Kagustuhan
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user