mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-21 23:44:42 +00:00
Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f06460667 | ||
|
|
617f1bf2c9 | ||
|
|
02ad38f871 | ||
|
|
7a2944cbea | ||
|
|
4c47204f92 | ||
|
|
f052395a7f | ||
|
|
57be83b38d | ||
|
|
c2cd4f5bbf | ||
|
|
d3d57312ba | ||
|
|
593a64c9bd | ||
|
|
19dc4fb6ff | ||
|
|
81f45012f3 | ||
|
|
f91cc2374c | ||
|
|
48298bb161 | ||
|
|
7a1cd9026c | ||
|
|
754e53d8db | ||
|
|
c36f1bc8d0 | ||
|
|
8a243a01aa | ||
|
|
9e4006cc89 | ||
|
|
26b69beb87 | ||
|
|
f95bf87a4b | ||
|
|
e854c7d189 | ||
|
|
8a434dcd96 | ||
|
|
6b7324723e | ||
|
|
0bdcb2b3be | ||
|
|
c3931d9d29 | ||
|
|
307825a339 | ||
|
|
093f0e8c94 | ||
|
|
c938c42c00 | ||
|
|
59560193ee | ||
|
|
a6b31e19b9 | ||
|
|
8a44115234 | ||
|
|
43a1f00bea | ||
|
|
3e458060bc | ||
|
|
4bb5a3f10d | ||
|
|
cdcd43a805 | ||
|
|
d5245009f4 | ||
|
|
8f4392711e | ||
|
|
79bb4a5215 | ||
|
|
2ce7fee06d | ||
|
|
99e9e92f10 | ||
|
|
8e6500d93f | ||
|
|
89ce99deaf | ||
|
|
510e134605 | ||
|
|
62b434f549 | ||
|
|
1f1e336d57 | ||
|
|
02186ca17a | ||
|
|
b0ed133e05 |
@@ -20,6 +20,10 @@
|
|||||||
|
|
||||||
Translations are not managed directly in this repository. Instead, we use [Crowdin](https://translate.cryptomator.org/), which automatically synchronizes translations with this repository. If you want to help us with translations, please visit our translation project on Crowdin.
|
Translations are not managed directly in this repository. Instead, we use [Crowdin](https://translate.cryptomator.org/), which automatically synchronizes translations with this repository. If you want to help us with translations, please visit our translation project on Crowdin.
|
||||||
|
|
||||||
|
## Use of Generative AI
|
||||||
|
|
||||||
|
AI tools may assist your work, but every contribution must be fully understood, reviewed, and tested by you. Only submit changes you can clearly explain and justify. Unverified or low-quality AI output that wastes our time and resources will be closed without further review.
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md).
|
Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md).
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_DIST: 'temurin'
|
JAVA_DIST: 'temurin'
|
||||||
JAVA_VERSION: '24.0.1+9'
|
JAVA_VERSION: '25.0.1+8.0.LTS'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-version:
|
get-version:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_DIST: 'temurin'
|
JAVA_DIST: 'temurin'
|
||||||
JAVA_VERSION: 24
|
JAVA_VERSION: 25
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -65,7 +65,8 @@ jobs:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
TODO FULL CHANGELOG
|
### Full Changelog
|
||||||
|
See [CHANGELOG.md](https://github.com/cryptomator/cryptomator/blob/develop/CHANGELOG.md).
|
||||||
|
|
||||||
📜 List of closed issues is available [here](TODO)
|
📜 List of closed issues is available [here](TODO)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
JDK_VERSION: '24.0.1+9'
|
JDK_VERSION: '25.0.1+8.0.LTS'
|
||||||
JDK_VENDOR: temurin
|
JDK_VENDOR: temurin
|
||||||
RUNTIME_VERSION_HELPER: >
|
RUNTIME_VERSION_HELPER: >
|
||||||
public class Test {
|
public class Test {
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
JDK_MAJOR_VERSION: 'toBeFilled'
|
JDK_MAJOR_VERSION: 'toBeFilled'
|
||||||
steps:
|
steps:
|
||||||
- name: Determine current major version
|
- name: Determine current major version
|
||||||
run: echo 'JDK_MAJOR_VERSION=${{ env.JDK_VERSION }}'.substring(0,20) >> "$env:GITHUB_ENV"
|
run: echo 'JDK_MAJOR_VERSION=${{ env.JDK_VERSION }}'.substring(0,2) >> "$env:GITHUB_ENV"
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: Checkout latest JDK ${{ env.JDK_MAJOR_VERSION }}
|
- name: Checkout latest JDK ${{ env.JDK_MAJOR_VERSION }}
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
|
|||||||
@@ -23,9 +23,8 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_DIST: 'temurin'
|
JAVA_DIST: 'temurin'
|
||||||
JAVA_VERSION: '24.0.1+9'
|
JAVA_VERSION: '25.0.1+8.0.LTS'
|
||||||
COFFEELIBS_JDK: 24
|
DEB_BUILD_DEPENDS: 'debhelper (>=10), openjdk-25-jdk (>= 25+36), libgtk-3-0 (>= 3.20.0), libxxf86vm1, libgl1'
|
||||||
COFFEELIBS_JDK_VERSION: '24.0.1+9-0ppa3'
|
|
||||||
OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_linux-x64_bin-jmods.zip'
|
OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_linux-x64_bin-jmods.zip'
|
||||||
OPENJFX_JMODS_AMD64_HASH: '96e520f48610d8ffb94ca30face1f11ffe8a977ddc1c4ff80b1a9e9f048bd94e'
|
OPENJFX_JMODS_AMD64_HASH: '96e520f48610d8ffb94ca30face1f11ffe8a977ddc1c4ff80b1a9e9f048bd94e'
|
||||||
OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_linux-aarch64_bin-jmods.zip'
|
OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_linux-aarch64_bin-jmods.zip'
|
||||||
@@ -55,9 +54,11 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Install build tools
|
- name: Install build tools
|
||||||
run: |
|
run: |
|
||||||
sudo add-apt-repository ppa:coffeelibs/openjdk
|
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.COFFEELIBS_JDK }}=${{ env.COFFEELIBS_JDK_VERSION }}
|
sudo apt-get install devscripts dput
|
||||||
|
sudo apt-get satisfy "${DEB_BUILD_DEPENDS}"
|
||||||
|
env:
|
||||||
|
DEB_BUILD_DEPENDS: ${{ env.DEB_BUILD_DEPENDS }}
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
runner-os: 'ubuntu-latest'
|
runner-os: 'ubuntu-latest'
|
||||||
java-distribution: 'temurin'
|
java-distribution: 'temurin'
|
||||||
java-version: 24
|
java-version: 25
|
||||||
secrets:
|
secrets:
|
||||||
nvd-api-key: ${{ secrets.NVD_API_KEY }}
|
nvd-api-key: ${{ secrets.NVD_API_KEY }}
|
||||||
ossindex-username: ${{ secrets.OSSINDEX_USERNAME }}
|
ossindex-username: ${{ secrets.OSSINDEX_USERNAME }}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_DIST: 'temurin'
|
JAVA_DIST: 'temurin'
|
||||||
JAVA_VERSION: 24
|
JAVA_VERSION: 25
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
determine-version:
|
determine-version:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_DIST: 'temurin'
|
JAVA_DIST: 'temurin'
|
||||||
JAVA_VERSION: '24.0.1+9'
|
JAVA_VERSION: '25.0.1+8.0.LTS'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-version:
|
get-version:
|
||||||
@@ -136,6 +136,7 @@ jobs:
|
|||||||
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
|
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
|
||||||
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\""
|
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\""
|
||||||
--java-options "-Dcryptomator.showTrayIcon=true"
|
--java-options "-Dcryptomator.showTrayIcon=true"
|
||||||
|
--java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism"
|
||||||
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\""
|
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\""
|
||||||
--mac-package-identifier org.cryptomator
|
--mac-package-identifier org.cryptomator
|
||||||
--resource-dir dist/mac/resources
|
--resource-dir dist/mac/resources
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_DIST: 'temurin'
|
JAVA_DIST: 'temurin'
|
||||||
JAVA_VERSION: '24.0.1+9'
|
JAVA_VERSION: '25.0.1+8.0.LTS'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-version:
|
get-version:
|
||||||
@@ -134,6 +134,7 @@ jobs:
|
|||||||
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
|
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
|
||||||
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\""
|
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\""
|
||||||
--java-options "-Dcryptomator.showTrayIcon=true"
|
--java-options "-Dcryptomator.showTrayIcon=true"
|
||||||
|
--java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism"
|
||||||
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\""
|
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\""
|
||||||
--java-options "-XX:ErrorFile=/cryptomator/cryptomator_crash.log"
|
--java-options "-XX:ErrorFile=/cryptomator/cryptomator_crash.log"
|
||||||
--mac-package-identifier org.cryptomator
|
--mac-package-identifier org.cryptomator
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_DIST: 'temurin'
|
JAVA_DIST: 'temurin'
|
||||||
JAVA_VERSION: 24
|
JAVA_VERSION: 25
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ defaults:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_DIST: 'temurin'
|
JAVA_DIST: 'temurin'
|
||||||
JAVA_VERSION: 23
|
JAVA_VERSION: 25
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-preconditions:
|
check-preconditions:
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- arch: x64
|
- arch: x64
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
java-dist: 'zulu'
|
java-dist: 'zulu' #cannot use temurin, see https://github.com/cryptomator/cryptomator/issues/3824#issuecomment-2829827427
|
||||||
java-version: '24.0.1+9'
|
java-version: '25.0.1+8'
|
||||||
java-package: 'jdk'
|
java-package: 'jdk'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|||||||
Generated
+11
-10
@@ -14,16 +14,15 @@
|
|||||||
<option name="dagger.fastInit" value="enabled" />
|
<option name="dagger.fastInit" value="enabled" />
|
||||||
<option name="dagger.formatGeneratedSource" value="enabled" />
|
<option name="dagger.formatGeneratedSource" value="enabled" />
|
||||||
<processorPath useClasspath="false">
|
<processorPath useClasspath="false">
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.55/dagger-compiler-2.55.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.57.2/dagger-compiler-2.57.2.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.55/dagger-2.55.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.57.2/dagger-2.57.2.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar" />
|
<entry name="$MAVEN_REPOSITORY$/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.55/dagger-spi-2.55.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.57.2/dagger-spi-2.57.2.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/devtools/ksp/symbol-processing-api/2.0.21-1.0.28/symbol-processing-api-2.0.21-1.0.28.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/devtools/ksp/symbol-processing-api/2.1.21-2.0.2/symbol-processing-api-2.1.21-2.0.2.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.0.21/kotlin-stdlib-2.0.21.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.2/failureaccess-1.0.2.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.2/failureaccess-1.0.2.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/33.0.0-jre/guava-33.0.0-jre.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/33.0.0-jre/guava-33.0.0-jre.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
|
||||||
@@ -31,14 +30,16 @@
|
|||||||
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.23.0/error_prone_annotations-2.23.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.23.0/error_prone_annotations-2.23.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/2.8/j2objc-annotations-2.8.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/2.8/j2objc-annotations-2.8.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.13.0/javapoet-1.13.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.13.0/javapoet-1.13.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/squareup/kotlinpoet/1.11.0/kotlinpoet-1.11.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/squareup/kotlinpoet/1.11.0/kotlinpoet-1.11.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.6.10/kotlin-stdlib-jdk8-1.6.10.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.6.10/kotlin-stdlib-jdk8-1.6.10.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.6.10/kotlin-stdlib-jdk7-1.6.10.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.6.10/kotlin-stdlib-jdk7-1.6.10.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.6.10/kotlin-reflect-1.6.10.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.6.10/kotlin-reflect-1.6.10.jar" />
|
||||||
|
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/net/ltgt/gradle/incap/incap/0.2/incap-0.2.jar" />
|
<entry name="$MAVEN_REPOSITORY$/net/ltgt/gradle/incap/incap/0.2/incap-0.2.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.5/checker-compat-qual-2.5.5.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" />
|
||||||
|
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-metadata-jvm/2.1.21/kotlin-metadata-jvm-2.1.21.jar" />
|
||||||
|
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.1.21/kotlin-stdlib-2.1.21.jar" />
|
||||||
|
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
|
||||||
</processorPath>
|
</processorPath>
|
||||||
<module name="cryptomator" />
|
<module name="cryptomator" />
|
||||||
</profile>
|
</profile>
|
||||||
|
|||||||
+3
-5
@@ -1,10 +1,8 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
<component name="InspectionProjectProfileManager">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
<inspection_tool class="SpellCheckingInspection" enabled="true" level="TYPO" enabled_by_default="true">
|
<inspection_tool class="Deprecation" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="DEPRECATED_ATTRIBUTES" />
|
||||||
<option name="processCode" value="true" />
|
<inspection_tool class="MarkedForRemoval" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="MARKED_FOR_REMOVAL_ATTRIBUTES" />
|
||||||
<option name="processLiterals" value="true" />
|
<inspection_tool class="RedundantScheduledForRemovalAnnotation" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="MARKED_FOR_REMOVAL_ATTRIBUTES" />
|
||||||
<option name="processComments" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
Generated
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_24" project-jdk-name="25" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="25" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
+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="@{userhome}/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.p12Path="@{userhome}/Library/Application Support/Cryptomator/key.p12" -Dcryptomator.ipcSocketPath="@{userhome}/Library/Application Support/Cryptomator/ipc.socket" -Dcryptomator.logDir="@{userhome}/Library/Logs/Cryptomator" -Dcryptomator.pluginDir="@{userhome}/Library/Application Support/Cryptomator/Plugins" -Dcryptomator.mountPointsDir="@{userhome}/Cryptomator" -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac,javafx.graphics" />
|
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath="@{userhome}/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.p12Path="@{userhome}/Library/Application Support/Cryptomator/key.p12" -Dcryptomator.ipcSocketPath="@{userhome}/Library/Application Support/Cryptomator/ipc.socket" -Dcryptomator.logDir="@{userhome}/Library/Logs/Cryptomator" -Dcryptomator.pluginDir="@{userhome}/Library/Application Support/Cryptomator/Plugins" -Dcryptomator.mountPointsDir="@{userhome}/Cryptomator" -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac,javafx.graphics" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
The changelog starts with version 1.19.0.
|
||||||
|
Changes to prior versions can be found on the [Github release page](https://github.com/cryptomator/cryptomator/releases).
|
||||||
|
|
||||||
|
## [Unreleased](https://github.com/cryptomator/cryptomator/compare/1.18.0...HEAD)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* New Self-Update Mechanism (initially for .dmg and Flatpak packages) (#3948)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Built using JDK 25 (#4031)
|
||||||
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-24 (>= 24.0.1+9-0ppa3), libgtk-3-0 (>= 3.20.0), libxxf86vm1, libgl1
|
Build-Depends: debhelper (>=10), openjdk-25-jdk (>= 25+36), libgtk-3-0 (>= 3.20.0), libxxf86vm1, libgl1
|
||||||
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
+2
-1
@@ -4,11 +4,12 @@
|
|||||||
# Uncomment this to turn on verbose mode.
|
# Uncomment this to turn on verbose mode.
|
||||||
#export DH_VERBOSE=1
|
#export DH_VERBOSE=1
|
||||||
|
|
||||||
JAVA_HOME = /usr/lib/jvm/java-24-coffeelibs
|
|
||||||
DEB_BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH)
|
DEB_BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH)
|
||||||
ifeq ($(DEB_BUILD_ARCH),amd64)
|
ifeq ($(DEB_BUILD_ARCH),amd64)
|
||||||
|
JAVA_HOME = /usr/lib/jvm/java-25-openjdk-amd64
|
||||||
JMODS_PATH = jmods/amd64:${JAVA_HOME}/jmods
|
JMODS_PATH = jmods/amd64:${JAVA_HOME}/jmods
|
||||||
else ifeq ($(DEB_BUILD_ARCH),arm64)
|
else ifeq ($(DEB_BUILD_ARCH),arm64)
|
||||||
|
JAVA_HOME = /usr/lib/jvm/java-25-openjdk-arm64
|
||||||
JMODS_PATH = jmods/aarch64:${JAVA_HOME}/jmods
|
JMODS_PATH = jmods/aarch64:${JAVA_HOME}/jmods
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
@@ -123,6 +123,7 @@ ${JAVA_HOME}/bin/jpackage \
|
|||||||
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"${APP_NAME}\"" \
|
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"${APP_NAME}\"" \
|
||||||
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support${APP_NAME}/mnt\"" \
|
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support${APP_NAME}/mnt\"" \
|
||||||
--java-options "-Dcryptomator.showTrayIcon=true" \
|
--java-options "-Dcryptomator.showTrayIcon=true" \
|
||||||
|
--java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" \
|
||||||
--java-options "-Dcryptomator.buildNumber=\"dmg-${REVISION_NO}\"" \
|
--java-options "-Dcryptomator.buildNumber=\"dmg-${REVISION_NO}\"" \
|
||||||
--mac-package-identifier ${PACKAGE_IDENTIFIER} \
|
--mac-package-identifier ${PACKAGE_IDENTIFIER} \
|
||||||
--resource-dir ../resources
|
--resource-dir ../resources
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
Apache License v2.0|Apache License, Version 2.0|The Apache Software License, Version 2.0|Apache 2.0|Apache Software License - Version 2.0|Apache-2.0
|
Apache License v2.0|Apache License, Version 2.0|The Apache License, Version 2.0|The Apache Software License, Version 2.0|Apache 2.0|Apache Software License - Version 2.0|Apache-2.0
|
||||||
MIT License|The MIT License (MIT)|The MIT License|MIT license
|
MIT License|MIT|The MIT License (MIT)|The MIT License|MIT license
|
||||||
LGPL 2.1|LGPL, version 2.1|GNU Lesser/Library General Public License version 2|GNU Lesser General Public License Version 2.1
|
LGPL 2.1|LGPL, version 2.1|GNU Lesser/Library General Public License version 2|GNU Lesser General Public License Version 2.1|GNU Lesser General Public License
|
||||||
GPLv2|GNU General Public License Version 2
|
GPLv2|GNU General Public License Version 2
|
||||||
GPLv2+CE|CDDL + GPLv2 with classpath exception
|
GPLv2+CE|CDDL + GPLv2 with classpath exception
|
||||||
Eclipse Public License - Version 1.0|Eclipse Public License - v 1.0
|
Eclipse Public License - Version 1.0|Eclipse Public License - v 1.0
|
||||||
|
|||||||
@@ -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.18.0</version>
|
<version>1.19.0</version>
|
||||||
<name>Cryptomator Desktop App</name>
|
<name>Cryptomator Desktop App</name>
|
||||||
|
|
||||||
<organization>
|
<organization>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.jdk.version>24</project.jdk.version>
|
<project.jdk.version>25</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 -->
|
||||||
<!-- remove them, as soon they got modularized or support is dropped (i.e., WebDAV) -->
|
<!-- remove them, as soon they got modularized or support is dropped (i.e., WebDAV) -->
|
||||||
@@ -34,10 +34,10 @@
|
|||||||
|
|
||||||
<!-- cryptomator dependencies -->
|
<!-- cryptomator dependencies -->
|
||||||
<cryptomator.cryptofs.version>2.9.0</cryptomator.cryptofs.version>
|
<cryptomator.cryptofs.version>2.9.0</cryptomator.cryptofs.version>
|
||||||
<cryptomator.integrations.version>1.7.0</cryptomator.integrations.version>
|
<cryptomator.integrations.version>1.8.0-beta1</cryptomator.integrations.version>
|
||||||
<cryptomator.integrations.win.version>1.5.1</cryptomator.integrations.win.version>
|
<cryptomator.integrations.win.version>1.5.1</cryptomator.integrations.win.version>
|
||||||
<cryptomator.integrations.mac.version>1.4.1</cryptomator.integrations.mac.version>
|
<cryptomator.integrations.mac.version>1.5.0-beta1</cryptomator.integrations.mac.version>
|
||||||
<cryptomator.integrations.linux.version>1.6.1</cryptomator.integrations.linux.version>
|
<cryptomator.integrations.linux.version>1.7.0-beta1</cryptomator.integrations.linux.version>
|
||||||
<cryptomator.fuse.version>5.1.0</cryptomator.fuse.version>
|
<cryptomator.fuse.version>5.1.0</cryptomator.fuse.version>
|
||||||
<cryptomator.webdav.version>3.0.0</cryptomator.webdav.version>
|
<cryptomator.webdav.version>3.0.0</cryptomator.webdav.version>
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<!-- build-time dependencies -->
|
<!-- build-time dependencies -->
|
||||||
<jetbrains.annotations.version>26.0.2-1</jetbrains.annotations.version>
|
<jetbrains.annotations.version>26.0.2-1</jetbrains.annotations.version>
|
||||||
<dependency-check.version>12.1.5</dependency-check.version>
|
<dependency-check.version>12.1.5</dependency-check.version>
|
||||||
<jacoco.version>0.8.13</jacoco.version>
|
<jacoco.version>0.8.14</jacoco.version>
|
||||||
<license-generator.version>2.7.0</license-generator.version>
|
<license-generator.version>2.7.0</license-generator.version>
|
||||||
<junit-tree-reporter.version>1.4.0</junit-tree-reporter.version>
|
<junit-tree-reporter.version>1.4.0</junit-tree-reporter.version>
|
||||||
<mvn-compiler.version>3.14.1</mvn-compiler.version>
|
<mvn-compiler.version>3.14.1</mvn-compiler.version>
|
||||||
@@ -75,6 +75,20 @@
|
|||||||
<surefire.jacoco.args></surefire.jacoco.args>
|
<surefire.jacoco.args></surefire.jacoco.args>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<name>Central Portal Snapshots</name>
|
||||||
|
<id>central-portal-snapshots</id>
|
||||||
|
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- Cryptomator Libs -->
|
<!-- Cryptomator Libs -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ open module org.cryptomator.desktop {
|
|||||||
requires io.github.coffeelibs.tinyoauth2client;
|
requires io.github.coffeelibs.tinyoauth2client;
|
||||||
requires org.slf4j;
|
requires org.slf4j;
|
||||||
requires org.apache.commons.lang3;
|
requires org.apache.commons.lang3;
|
||||||
|
requires com.github.benmanes.caffeine;
|
||||||
|
|
||||||
/* dagger bs */
|
/* dagger bs */
|
||||||
requires jakarta.inject;
|
requires jakarta.inject;
|
||||||
requires static javax.inject;
|
requires static javax.inject;
|
||||||
requires java.compiler;
|
requires java.compiler;
|
||||||
requires com.github.benmanes.caffeine;
|
|
||||||
|
|
||||||
uses org.cryptomator.common.locationpresets.LocationPresetsProvider;
|
uses org.cryptomator.common.locationpresets.LocationPresetsProvider;
|
||||||
uses SSLContextProvider;
|
uses SSLContextProvider;
|
||||||
|
|||||||
@@ -74,13 +74,6 @@ public abstract class CommonsModule {
|
|||||||
return new MasterkeyFileAccess(Constants.PEPPER, csprng);
|
return new MasterkeyFileAccess(Constants.PEPPER, csprng);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
@Named("SemVer")
|
|
||||||
static Comparator<String> providesSemVerComparator() {
|
|
||||||
return new SemVerComparator();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
static Optional<RevealPathService> provideRevealPathService() {
|
static Optional<RevealPathService> provideRevealPathService() {
|
||||||
|
|||||||
@@ -124,6 +124,15 @@ public class Environment {
|
|||||||
return Optional.ofNullable(System.getProperty(BUILD_NUMBER_PROP_NAME));
|
return Optional.ofNullable(System.getProperty(BUILD_NUMBER_PROP_NAME));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the app version concatenated with the build number (if defined).
|
||||||
|
*
|
||||||
|
* @return version string formatted like {@code 1.2.3-4567} or {@code 1.2.3} if no build number is defined.
|
||||||
|
*/
|
||||||
|
public String getAppVersionWithBuildNumber() {
|
||||||
|
return getAppVersion() + getBuildNumber().map("-"::concat).orElse("");
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<Path> getPluginDir() {
|
public Optional<Path> getPluginDir() {
|
||||||
return getPath(PLUGIN_DIR_PROP_NAME);
|
return getPath(PLUGIN_DIR_PROP_NAME);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
|
||||||
* All rights reserved.
|
|
||||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
|
||||||
*
|
|
||||||
* Contributors:
|
|
||||||
* Sebastian Stenzel - initial API and implementation
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.common;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares version strings according to <a href="http://semver.org/spec/v2.0.0.html">SemVer 2.0.0</a>.
|
|
||||||
*/
|
|
||||||
public class SemVerComparator implements Comparator<String> {
|
|
||||||
|
|
||||||
private static final char VERSION_SEP = '.'; // http://semver.org/spec/v2.0.0.html#spec-item-2
|
|
||||||
private static final String PRE_RELEASE_SEP = "-"; // http://semver.org/spec/v2.0.0.html#spec-item-9
|
|
||||||
private static final String BUILD_SEP = "+"; // http://semver.org/spec/v2.0.0.html#spec-item-10
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compare(String version1, String version2) {
|
|
||||||
// "Build metadata SHOULD be ignored when determining version precedence.
|
|
||||||
// Thus two versions that differ only in the build metadata, have the same precedence."
|
|
||||||
String v1WithoutBuildMetadata = StringUtils.substringBefore(version1, BUILD_SEP);
|
|
||||||
String v2WithoutBuildMetadata = StringUtils.substringBefore(version2, BUILD_SEP);
|
|
||||||
|
|
||||||
if (v1WithoutBuildMetadata.equals(v2WithoutBuildMetadata)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
String v1MajorMinorPatch = StringUtils.substringBefore(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
|
||||||
String v2MajorMinorPatch = StringUtils.substringBefore(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
|
||||||
String v1PreReleaseVersion = StringUtils.substringAfter(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
|
||||||
String v2PreReleaseVersion = StringUtils.substringAfter(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
|
||||||
return compare(v1MajorMinorPatch, v1PreReleaseVersion, v2MajorMinorPatch, v2PreReleaseVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int compare(String v1MajorMinorPatch, String v1PreReleaseVersion, String v2MajorMinorPatch, String v2PreReleaseVersion) {
|
|
||||||
int comparisonResult = compareNumericallyThenLexicographically(v1MajorMinorPatch, v2MajorMinorPatch);
|
|
||||||
if (comparisonResult == 0) {
|
|
||||||
if (v1PreReleaseVersion.isEmpty()) {
|
|
||||||
return 1; // 1.0.0 > 1.0.0-BETA
|
|
||||||
} else if (v2PreReleaseVersion.isEmpty()) {
|
|
||||||
return -1; // 1.0.0-BETA < 1.0.0
|
|
||||||
} else {
|
|
||||||
return compareNumericallyThenLexicographically(v1PreReleaseVersion, v2PreReleaseVersion);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return comparisonResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int compareNumericallyThenLexicographically(String version1, String version2) {
|
|
||||||
final String[] vComps1 = StringUtils.split(version1, VERSION_SEP);
|
|
||||||
final String[] vComps2 = StringUtils.split(version2, VERSION_SEP);
|
|
||||||
final int commonCompCount = Math.min(vComps1.length, vComps2.length);
|
|
||||||
|
|
||||||
for (int i = 0; i < commonCompCount; i++) {
|
|
||||||
int subversionComparisonResult = 0;
|
|
||||||
try {
|
|
||||||
final int v1 = Integer.parseInt(vComps1[i]);
|
|
||||||
final int v2 = Integer.parseInt(vComps2[i]);
|
|
||||||
subversionComparisonResult = v1 - v2;
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
// ok, lets compare this fragment lexicographically
|
|
||||||
subversionComparisonResult = vComps1[i].compareTo(vComps2[i]);
|
|
||||||
}
|
|
||||||
if (subversionComparisonResult != 0) {
|
|
||||||
return subversionComparisonResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// all in common so far? longest version string is considered the higher version:
|
|
||||||
return vComps1.length - vComps2.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -25,10 +25,8 @@ import javafx.beans.property.StringProperty;
|
|||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.geometry.NodeOrientation;
|
import javafx.geometry.NodeOrientation;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public class Settings {
|
public class Settings {
|
||||||
|
|
||||||
@@ -53,6 +51,7 @@ public class Settings {
|
|||||||
static final String DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT.name();
|
static final String DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT.name();
|
||||||
public static final Instant DEFAULT_TIMESTAMP = Instant.parse("2000-01-01T00:00:00Z");
|
public static final Instant DEFAULT_TIMESTAMP = Instant.parse("2000-01-01T00:00:00Z");
|
||||||
|
|
||||||
|
private final SettingsProvider provider;
|
||||||
public final ObservableList<VaultSettings> directories;
|
public final ObservableList<VaultSettings> directories;
|
||||||
public final BooleanProperty startHidden;
|
public final BooleanProperty startHidden;
|
||||||
public final BooleanProperty autoCloseVaults;
|
public final BooleanProperty autoCloseVaults;
|
||||||
@@ -78,13 +77,12 @@ public class Settings {
|
|||||||
public final ObjectProperty<Instant> lastUpdateCheckReminder;
|
public final ObjectProperty<Instant> lastUpdateCheckReminder;
|
||||||
public final ObjectProperty<Instant> lastSuccessfulUpdateCheck;
|
public final ObjectProperty<Instant> lastSuccessfulUpdateCheck;
|
||||||
public final ObjectProperty<Path> previouslyUsedVaultDirectory;
|
public final ObjectProperty<Path> previouslyUsedVaultDirectory;
|
||||||
|
public final StringProperty lastUpdateAttemptedByVersion;
|
||||||
|
|
||||||
private Consumer<Settings> saveCmd;
|
public static Settings create(SettingsProvider provider, Environment env) {
|
||||||
|
|
||||||
public static Settings create(Environment env) {
|
|
||||||
var defaults = new SettingsJson();
|
var defaults = new SettingsJson();
|
||||||
defaults.showTrayIcon = env.showTrayIcon();
|
defaults.showTrayIcon = env.showTrayIcon();
|
||||||
return new Settings(defaults);
|
return new Settings(provider, defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +90,8 @@ public class Settings {
|
|||||||
*
|
*
|
||||||
* @param json The parsed settings.json
|
* @param json The parsed settings.json
|
||||||
*/
|
*/
|
||||||
Settings(SettingsJson json) {
|
Settings(SettingsProvider provider, SettingsJson json) {
|
||||||
|
this.provider = provider;
|
||||||
this.directories = FXCollections.observableArrayList(VaultSettings::observables);
|
this.directories = FXCollections.observableArrayList(VaultSettings::observables);
|
||||||
this.startHidden = new SimpleBooleanProperty(this, "startHidden", json.startHidden);
|
this.startHidden = new SimpleBooleanProperty(this, "startHidden", json.startHidden);
|
||||||
this.autoCloseVaults = new SimpleBooleanProperty(this, "autoCloseVaults", json.autoCloseVaults);
|
this.autoCloseVaults = new SimpleBooleanProperty(this, "autoCloseVaults", json.autoCloseVaults);
|
||||||
@@ -118,6 +117,7 @@ public class Settings {
|
|||||||
this.lastUpdateCheckReminder = new SimpleObjectProperty<>(this, "lastUpdateCheckReminder", json.lastReminderForUpdateCheck);
|
this.lastUpdateCheckReminder = new SimpleObjectProperty<>(this, "lastUpdateCheckReminder", json.lastReminderForUpdateCheck);
|
||||||
this.lastSuccessfulUpdateCheck = new SimpleObjectProperty<>(this, "lastSuccessfulUpdateCheck", json.lastSuccessfulUpdateCheck);
|
this.lastSuccessfulUpdateCheck = new SimpleObjectProperty<>(this, "lastSuccessfulUpdateCheck", json.lastSuccessfulUpdateCheck);
|
||||||
this.previouslyUsedVaultDirectory = new SimpleObjectProperty<>(this, "previouslyUsedVaultDirectory", json.previouslyUsedVaultDirectory);
|
this.previouslyUsedVaultDirectory = new SimpleObjectProperty<>(this, "previouslyUsedVaultDirectory", json.previouslyUsedVaultDirectory);
|
||||||
|
this.lastUpdateAttemptedByVersion = new SimpleStringProperty(this, "lastUpdateAttemptedByVersion", json.lastUpdateAttemptedByVersion);
|
||||||
|
|
||||||
this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList());
|
this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList());
|
||||||
|
|
||||||
@@ -148,6 +148,7 @@ public class Settings {
|
|||||||
lastUpdateCheckReminder.addListener(this::somethingChanged);
|
lastUpdateCheckReminder.addListener(this::somethingChanged);
|
||||||
lastSuccessfulUpdateCheck.addListener(this::somethingChanged);
|
lastSuccessfulUpdateCheck.addListener(this::somethingChanged);
|
||||||
previouslyUsedVaultDirectory.addListener(this::somethingChanged);
|
previouslyUsedVaultDirectory.addListener(this::somethingChanged);
|
||||||
|
lastUpdateAttemptedByVersion.addListener(this::somethingChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@@ -210,6 +211,7 @@ public class Settings {
|
|||||||
json.lastReminderForUpdateCheck = lastUpdateCheckReminder.get();
|
json.lastReminderForUpdateCheck = lastUpdateCheckReminder.get();
|
||||||
json.lastSuccessfulUpdateCheck = lastSuccessfulUpdateCheck.get();
|
json.lastSuccessfulUpdateCheck = lastSuccessfulUpdateCheck.get();
|
||||||
json.previouslyUsedVaultDirectory = previouslyUsedVaultDirectory.get();
|
json.previouslyUsedVaultDirectory = previouslyUsedVaultDirectory.get();
|
||||||
|
json.lastUpdateAttemptedByVersion = lastUpdateAttemptedByVersion.get();
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,20 +224,12 @@ public class Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO rename to setChangeListener
|
|
||||||
void setSaveCmd(Consumer<Settings> saveCmd) {
|
|
||||||
this.saveCmd = saveCmd;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
|
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
|
||||||
this.save();
|
provider.scheduleSave(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void save() {
|
public void saveNow() {
|
||||||
if (saveCmd != null) {
|
provider.saveNow(this);
|
||||||
saveCmd.accept(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,4 +96,7 @@ class SettingsJson {
|
|||||||
|
|
||||||
@JsonProperty("previouslyUsedVaultDirectory")
|
@JsonProperty("previouslyUsedVaultDirectory")
|
||||||
Path previouslyUsedVaultDirectory;
|
Path previouslyUsedVaultDirectory;
|
||||||
|
|
||||||
|
@JsonProperty("lastUpdateAttemptedByVersion")
|
||||||
|
String lastUpdateAttemptedByVersion;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ import java.nio.file.NoSuchFileException;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.Optional;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -61,8 +63,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
|||||||
Settings settings = env.getSettingsPath() //
|
Settings settings = env.getSettingsPath() //
|
||||||
.flatMap(this::tryLoad) //
|
.flatMap(this::tryLoad) //
|
||||||
.findFirst() //
|
.findFirst() //
|
||||||
.orElseGet(() -> Settings.create(env));
|
.orElseGet(() -> Settings.create(this, env));
|
||||||
settings.setSaveCmd(this::scheduleSave);
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
|||||||
try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) {
|
try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) {
|
||||||
var json = JSON.reader().readValue(in, SettingsJson.class);
|
var json = JSON.reader().readValue(in, SettingsJson.class);
|
||||||
LOG.info("Settings loaded from {}", path);
|
LOG.info("Settings loaded from {}", path);
|
||||||
var settings = new Settings(json);
|
var settings = new Settings(this, json);
|
||||||
return Stream.of(settings);
|
return Stream.of(settings);
|
||||||
} catch (JacksonException e) {
|
} catch (JacksonException e) {
|
||||||
LOG.warn("Failed to parse json file {}", path, e);
|
LOG.warn("Failed to parse json file {}", path, e);
|
||||||
@@ -84,19 +85,33 @@ public class SettingsProvider implements Supplier<Settings> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleSave(Settings settings) {
|
void saveNow(Settings settings) {
|
||||||
if (settings == null) {
|
try {
|
||||||
return;
|
scheduleSave(settings, 0L).get();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
LOG.error("Saving settings was interrupted.", e);
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
LOG.error("Unexpected exception while saving.", e);
|
||||||
}
|
}
|
||||||
final Optional<Path> settingsPath = env.getSettingsPath().findFirst(); // always save to preferred (first) path
|
}
|
||||||
settingsPath.ifPresent(path -> {
|
|
||||||
Runnable saveCommand = () -> this.save(settings, path);
|
void scheduleSave(Settings settings) {
|
||||||
ScheduledFuture<?> scheduledTask = scheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
|
scheduleSave(settings, SAVE_DELAY_MS);
|
||||||
ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
|
}
|
||||||
if (previouslyScheduledTask != null) {
|
|
||||||
previouslyScheduledTask.cancel(false);
|
private Future<?> scheduleSave(Settings settings, long delayMillis) {
|
||||||
}
|
if (settings == null) {
|
||||||
});
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
final Path settingsPath = env.getSettingsPath().findFirst().orElseThrow(); // always save to preferred (first) path
|
||||||
|
Runnable saveCommand = () -> this.save(settings, settingsPath);
|
||||||
|
ScheduledFuture<?> scheduledTask = scheduler.schedule(saveCommand, delayMillis, TimeUnit.MILLISECONDS);
|
||||||
|
ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
|
||||||
|
if (previouslyScheduledTask != null) {
|
||||||
|
previouslyScheduledTask.cancel(false);
|
||||||
|
}
|
||||||
|
return scheduledTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void save(Settings settings, Path settingsPath) {
|
private void save(Settings settings, Path settingsPath) {
|
||||||
@@ -107,7 +122,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
|||||||
Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
|
Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
|
||||||
try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
|
try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
|
||||||
var jsonObj = settings.serialized();
|
var jsonObj = settings.serialized();
|
||||||
jsonObj.writtenByVersion = env.getAppVersion() + env.getBuildNumber().map("-"::concat).orElse("");
|
jsonObj.writtenByVersion = env.getAppVersionWithBuildNumber();
|
||||||
JSON.writerWithDefaultPrettyPrinter().writeValue(out, jsonObj);
|
JSON.writerWithDefaultPrettyPrinter().writeValue(out, jsonObj);
|
||||||
}
|
}
|
||||||
Files.move(tmpPath, settingsPath, StandardCopyOption.REPLACE_EXISTING);
|
Files.move(tmpPath, settingsPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ public class DecryptFileNamesViewController implements FxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//obvservable getter
|
//observable getter
|
||||||
|
|
||||||
public ObservableValue<String> dropZoneTextProperty() {
|
public ObservableValue<String> dropZoneTextProperty() {
|
||||||
return dropZoneText;
|
return dropZoneText;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import javafx.scene.image.Image;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, //
|
@Module(subcomponents = {TrayMenuComponent.class, //
|
||||||
DecryptNameComponent.class, //
|
DecryptNameComponent.class, //
|
||||||
MainWindowComponent.class, //
|
MainWindowComponent.class, //
|
||||||
PreferencesComponent.class, //
|
PreferencesComponent.class, //
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
package org.cryptomator.ui.fxapp;
|
|
||||||
|
|
||||||
import org.cryptomator.common.Environment;
|
|
||||||
import org.cryptomator.common.SemVerComparator;
|
|
||||||
import org.cryptomator.common.settings.Settings;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.binding.BooleanBinding;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.ReadOnlyStringProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
|
||||||
import javafx.beans.property.StringProperty;
|
|
||||||
import javafx.concurrent.ScheduledService;
|
|
||||||
import javafx.concurrent.Worker;
|
|
||||||
import javafx.concurrent.WorkerStateEvent;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
@FxApplicationScoped
|
|
||||||
public class UpdateChecker {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class);
|
|
||||||
private static final Duration AUTO_CHECK_DELAY = Duration.seconds(5);
|
|
||||||
|
|
||||||
private final Environment env;
|
|
||||||
private final Settings settings;
|
|
||||||
private final StringProperty latestVersion = new SimpleStringProperty();
|
|
||||||
private final ScheduledService<String> updateCheckerService;
|
|
||||||
private final ObjectProperty<UpdateCheckState> state = new SimpleObjectProperty<>(UpdateCheckState.NOT_CHECKED);
|
|
||||||
private final ObjectProperty<Instant> lastSuccessfulUpdateCheck;
|
|
||||||
private final Comparator<String> versionComparator = new SemVerComparator();
|
|
||||||
private final BooleanBinding updateAvailable;
|
|
||||||
private final BooleanBinding checkFailed;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
UpdateChecker(Settings settings, //
|
|
||||||
Environment env, //
|
|
||||||
ScheduledService<String> updateCheckerService) {
|
|
||||||
this.env = env;
|
|
||||||
this.settings = settings;
|
|
||||||
this.updateCheckerService = updateCheckerService;
|
|
||||||
this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck;
|
|
||||||
this.updateAvailable = Bindings.createBooleanBinding(this::isUpdateAvailable, latestVersion);
|
|
||||||
this.checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void automaticallyCheckForUpdatesIfEnabled() {
|
|
||||||
if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) {
|
|
||||||
startCheckingForUpdates(AUTO_CHECK_DELAY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkForUpdatesNow() {
|
|
||||||
startCheckingForUpdates(Duration.ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startCheckingForUpdates(Duration initialDelay) {
|
|
||||||
updateCheckerService.cancel();
|
|
||||||
updateCheckerService.reset();
|
|
||||||
updateCheckerService.setDelay(initialDelay);
|
|
||||||
updateCheckerService.setOnRunning(this::checkStarted);
|
|
||||||
updateCheckerService.setOnSucceeded(this::checkSucceeded);
|
|
||||||
updateCheckerService.setOnFailed(this::checkFailed);
|
|
||||||
updateCheckerService.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkStarted(WorkerStateEvent event) {
|
|
||||||
LOG.debug("Checking for updates...");
|
|
||||||
state.set(UpdateCheckState.IS_CHECKING);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkSucceeded(WorkerStateEvent event) {
|
|
||||||
var latestVersionString = updateCheckerService.getValue();
|
|
||||||
LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), latestVersionString);
|
|
||||||
lastSuccessfulUpdateCheck.set(Instant.now());
|
|
||||||
latestVersion.set(latestVersionString);
|
|
||||||
state.set(UpdateCheckState.CHECK_SUCCESSFUL);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkFailed(WorkerStateEvent event) {
|
|
||||||
state.set(UpdateCheckState.CHECK_FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum UpdateCheckState {
|
|
||||||
NOT_CHECKED,
|
|
||||||
IS_CHECKING,
|
|
||||||
CHECK_SUCCESSFUL,
|
|
||||||
CHECK_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Observable Properties */
|
|
||||||
public BooleanBinding checkingForUpdatesProperty() {
|
|
||||||
return updateCheckerService.stateProperty().isEqualTo(Worker.State.RUNNING);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReadOnlyStringProperty latestVersionProperty() {
|
|
||||||
return latestVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BooleanBinding updateAvailableProperty() {
|
|
||||||
return updateAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BooleanBinding checkFailedProperty() {
|
|
||||||
return checkFailed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isUpdateAvailable() {
|
|
||||||
String currentVersion = getCurrentVersion();
|
|
||||||
String latestVersionString = latestVersion.get();
|
|
||||||
|
|
||||||
if (currentVersion == null || latestVersionString == null) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return versionComparator.compare(currentVersion, latestVersionString) < 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<Instant> lastSuccessfulUpdateCheckProperty() {
|
|
||||||
return lastSuccessfulUpdateCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<UpdateCheckState> updateCheckStateProperty() {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCurrentVersion() {
|
|
||||||
return env.getAppVersion();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package org.cryptomator.ui.fxapp;
|
|
||||||
|
|
||||||
import dagger.Module;
|
|
||||||
import dagger.Provides;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.Environment;
|
|
||||||
import org.cryptomator.common.settings.Settings;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Named;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.binding.ObjectBinding;
|
|
||||||
import javafx.concurrent.ScheduledService;
|
|
||||||
import javafx.concurrent.Task;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.HttpRequest;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
|
|
||||||
@Module
|
|
||||||
public abstract class UpdateCheckerModule {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerModule.class);
|
|
||||||
|
|
||||||
private static final URI LATEST_VERSION_URI = URI.create("https://api.cryptomator.org/desktop/latest-version.json");
|
|
||||||
private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3);
|
|
||||||
private static final Duration DISABLED_UPDATE_CHECK_INTERVAL = Duration.hours(100000); // Duration.INDEFINITE leads to overflows...
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@FxApplicationScoped
|
|
||||||
static Optional<HttpClient> provideHttpClient() {
|
|
||||||
try {
|
|
||||||
return Optional.of(HttpClient.newBuilder() //
|
|
||||||
.followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes
|
|
||||||
.build());
|
|
||||||
} catch (UncheckedIOException e) {
|
|
||||||
LOG.error("HttpClient for update check cannot be created.", e);
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@FxApplicationScoped
|
|
||||||
static HttpRequest provideCheckForUpdatesRequest(Environment env) {
|
|
||||||
String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", //
|
|
||||||
env.getAppVersion(), //
|
|
||||||
SystemUtils.OS_NAME, //
|
|
||||||
SystemUtils.OS_VERSION, //
|
|
||||||
SystemUtils.OS_ARCH); //
|
|
||||||
return HttpRequest.newBuilder() //
|
|
||||||
.uri(LATEST_VERSION_URI) //
|
|
||||||
.header("User-Agent", userAgent) //
|
|
||||||
.timeout(java.time.Duration.ofSeconds(10))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Named("checkForUpdatesInterval")
|
|
||||||
@FxApplicationScoped
|
|
||||||
static ObjectBinding<Duration> provideCheckForUpdateInterval(Settings settings) {
|
|
||||||
return Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@FxApplicationScoped
|
|
||||||
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, Optional<HttpClient> httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
|
|
||||||
ScheduledService<String> service = new ScheduledService<>() {
|
|
||||||
@Override
|
|
||||||
protected Task<String> createTask() {
|
|
||||||
if (httpClient.isPresent()) {
|
|
||||||
return new UpdateCheckerTask(httpClient.get(), checkForUpdatesRequest);
|
|
||||||
} else {
|
|
||||||
return new Task<>() {
|
|
||||||
@Override
|
|
||||||
protected String call() {
|
|
||||||
throw new NullPointerException("No HttpClient present.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
service.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException()));
|
|
||||||
service.setExecutor(executor);
|
|
||||||
service.periodProperty().bind(period);
|
|
||||||
return service;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package org.cryptomator.ui.fxapp;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.google.common.io.ByteStreams;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javafx.concurrent.Task;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.HttpRequest;
|
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
|
|
||||||
public class UpdateCheckerTask extends Task<String> {
|
|
||||||
|
|
||||||
private static final ObjectMapper JSON = new ObjectMapper();
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerTask.class);
|
|
||||||
|
|
||||||
private static final long MAX_RESPONSE_SIZE = 10L * 1024; // 10kb should be sufficient. protect against flooding
|
|
||||||
|
|
||||||
private final HttpClient httpClient;
|
|
||||||
private final HttpRequest checkForUpdatesRequest;
|
|
||||||
|
|
||||||
UpdateCheckerTask(HttpClient httpClient, HttpRequest checkForUpdatesRequest) {
|
|
||||||
this.httpClient = httpClient;
|
|
||||||
this.checkForUpdatesRequest = checkForUpdatesRequest;
|
|
||||||
|
|
||||||
setOnFailed(event -> LOG.error("Failed to check for updates", getException()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String call() throws IOException, InterruptedException {
|
|
||||||
HttpResponse<InputStream> response = httpClient.send(checkForUpdatesRequest, HttpResponse.BodyHandlers.ofInputStream());
|
|
||||||
if (response.statusCode() == 200) {
|
|
||||||
return processBody(response);
|
|
||||||
} else {
|
|
||||||
throw new IOException("Unexpected HTTP response code " + response.statusCode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String processBody(HttpResponse<InputStream> response) throws IOException {
|
|
||||||
try (InputStream in = response.body(); //
|
|
||||||
InputStream limitedIn = ByteStreams.limit(in, MAX_RESPONSE_SIZE)) {
|
|
||||||
var json = JSON.reader().readTree(limitedIn);
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
|
||||||
return json.get("mac").asText();
|
|
||||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
|
||||||
return json.get("win").asText();
|
|
||||||
} else if (SystemUtils.IS_OS_LINUX) {
|
|
||||||
return json.get("linux").asText();
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("Unsupported operating system");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import org.cryptomator.common.vaults.Vault;
|
|||||||
import org.cryptomator.common.vaults.VaultListManager;
|
import org.cryptomator.common.vaults.VaultListManager;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
import org.cryptomator.updater.UpdateChecker;
|
||||||
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.cryptomator.ui.preferences;
|
|||||||
import com.google.common.io.CharStreams;
|
import com.google.common.io.CharStreams;
|
||||||
import org.cryptomator.common.Environment;
|
import org.cryptomator.common.Environment;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
import org.cryptomator.updater.UpdateChecker;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package org.cryptomator.ui.preferences;
|
|||||||
|
|
||||||
import org.cryptomator.common.Environment;
|
import org.cryptomator.common.Environment;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
import org.cryptomator.updater.UpdateChecker;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,37 @@ package org.cryptomator.ui.preferences;
|
|||||||
|
|
||||||
import org.cryptomator.common.Environment;
|
import org.cryptomator.common.Environment;
|
||||||
import org.cryptomator.common.settings.Settings;
|
import org.cryptomator.common.settings.Settings;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.integrations.update.UpdateStep;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
import org.cryptomator.ui.common.VaultService;
|
||||||
|
import org.cryptomator.updater.UpdateChecker;
|
||||||
|
import org.cryptomator.updater.FallbackUpdateInfo;
|
||||||
|
import org.cryptomator.updater.UpdateService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javafx.animation.PauseTransition;
|
import javafx.animation.PauseTransition;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
|
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.BooleanExpression;
|
||||||
import javafx.beans.binding.ObjectBinding;
|
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.ReadOnlyStringProperty;
|
import javafx.beans.property.ReadOnlyStringProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.concurrent.Worker;
|
||||||
|
import javafx.concurrent.WorkerStateEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.CheckBox;
|
import javafx.scene.control.CheckBox;
|
||||||
import javafx.scene.control.ContentDisplay;
|
import javafx.scene.control.ContentDisplay;
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -34,73 +46,64 @@ import java.util.ResourceBundle;
|
|||||||
@PreferencesScoped
|
@PreferencesScoped
|
||||||
public class UpdatesPreferencesController implements FxController {
|
public class UpdatesPreferencesController implements FxController {
|
||||||
|
|
||||||
private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
|
private static final Logger LOG = LoggerFactory.getLogger(UpdatesPreferencesController.class);
|
||||||
+ "?utm_source=cryptomator-desktop" //
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
|
||||||
+ "&utm_medium=update-notification&" //
|
|
||||||
+ "utm_campaign=app-update-%s";
|
|
||||||
|
|
||||||
private final Application application;
|
private final Application application;
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
private final Settings settings;
|
private final Settings settings;
|
||||||
private final UpdateChecker updateChecker;
|
private final UpdateChecker updateChecker;
|
||||||
private final ObjectBinding<ContentDisplay> checkForUpdatesButtonState;
|
private final UpdateService updateService;
|
||||||
private final ReadOnlyStringProperty latestVersion;
|
private final ObservableList<Vault> unlockedVaults;
|
||||||
private final ObservableValue<Instant> lastSuccessfulUpdateCheck;
|
private final VaultService vaultService;
|
||||||
private final StringBinding lastUpdateCheckMessage;
|
private final ObjectBinding<Worker<?>> worker;
|
||||||
|
private final BooleanExpression running;
|
||||||
|
private final StringBinding updateButtonTitle;
|
||||||
|
private final ObjectBinding<ContentDisplay> updateButtonState;
|
||||||
private final ObservableValue<String> timeDifferenceMessage;
|
private final ObservableValue<String> timeDifferenceMessage;
|
||||||
private final String currentVersion;
|
private final StringBinding lastUpdateCheckMessage;
|
||||||
private final BooleanBinding updateAvailable;
|
private final BooleanBinding prohibitUpdateWhileUnlocked;
|
||||||
private final BooleanBinding checkFailed;
|
private final BooleanBinding updateButtonDisabled;
|
||||||
|
private final StringProperty errorMessage = new SimpleStringProperty("");
|
||||||
private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false);
|
private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false);
|
||||||
private final DateTimeFormatter formatter;
|
|
||||||
private final BooleanBinding upToDate;
|
|
||||||
private final String downloadsUri;
|
|
||||||
|
|
||||||
/* FXML */
|
/* FXML */
|
||||||
public CheckBox checkForUpdatesCheckbox;
|
public CheckBox checkForUpdatesCheckbox;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker) {
|
UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, ObservableList<Vault> vaults, VaultService vaultService) {
|
||||||
this.application = application;
|
this.application = application;
|
||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
this.resourceBundle = resourceBundle;
|
this.resourceBundle = resourceBundle;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.updateChecker = updateChecker;
|
this.updateChecker = updateChecker;
|
||||||
this.checkForUpdatesButtonState = Bindings.when(updateChecker.checkingForUpdatesProperty()).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
|
this.updateService = new UpdateService(updateChecker.updateProperty());
|
||||||
this.latestVersion = updateChecker.latestVersionProperty();
|
this.unlockedVaults = vaults.filtered(Vault::isUnlocked);
|
||||||
this.lastSuccessfulUpdateCheck = updateChecker.lastSuccessfulUpdateCheckProperty();
|
this.vaultService = vaultService;
|
||||||
this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, lastSuccessfulUpdateCheck);
|
this.worker = Bindings.when(updateChecker.updateAvailableProperty()).<Worker<?>>then(this.updateService).otherwise(this.updateChecker);
|
||||||
this.currentVersion = environment.getAppVersion();
|
this.running = Bindings.createBooleanBinding(this::isRunning, updateService.stateProperty(), updateChecker.stateProperty());
|
||||||
this.updateAvailable = updateChecker.updateAvailableProperty();
|
this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, worker, updateService.stateProperty(), updateService.messageProperty());
|
||||||
this.formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
|
this.updateButtonState = Bindings.createObjectBinding(this::getUpdateButtonState, updateChecker.stateProperty(), updateService.stateProperty());
|
||||||
this.upToDate = updateChecker.updateCheckStateProperty().isEqualTo(UpdateChecker.UpdateCheckState.CHECK_SUCCESSFUL).and(latestVersion.isEqualTo(currentVersion));
|
this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, updateChecker.lastSuccessfulUpdateCheckProperty());
|
||||||
this.checkFailed = updateChecker.checkFailedProperty();
|
this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, updateChecker.lastSuccessfulUpdateCheckProperty());
|
||||||
this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, lastSuccessfulUpdateCheck);
|
this.prohibitUpdateWhileUnlocked = Bindings.createBooleanBinding(this::isProhibitUpdateWhileUnlocked, unlockedVaults, updateChecker.updateProperty());
|
||||||
this.downloadsUri = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(currentVersion, StandardCharsets.US_ASCII));
|
this.updateButtonDisabled = Bindings.when(worker.isEqualTo(updateChecker)).then(running).otherwise(prohibitUpdateWhileUnlocked.or(running));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
checkForUpdatesCheckbox.selectedProperty().bindBidirectional(settings.checkForUpdates);
|
checkForUpdatesCheckbox.selectedProperty().bindBidirectional(settings.checkForUpdates);
|
||||||
|
updateChecker.updateAvailableProperty().addListener((_, _, hasUpdate) -> {
|
||||||
upToDate.addListener((_, _, newVal) -> {
|
if (!hasUpdate) {
|
||||||
if (newVal) {
|
|
||||||
upToDateLabelVisible.set(true);
|
upToDateLabelVisible.set(true);
|
||||||
PauseTransition delay = new PauseTransition(javafx.util.Duration.seconds(5));
|
PauseTransition delay = new PauseTransition(javafx.util.Duration.seconds(5));
|
||||||
delay.setOnFinished(_ -> upToDateLabelVisible.set(false));
|
delay.setOnFinished(_ -> upToDateLabelVisible.set(false));
|
||||||
delay.play();
|
delay.play();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
updateChecker.setOnFailed(this::checkFailed);
|
||||||
|
updateService.setOnSucceeded(this::updateSucceeded);
|
||||||
@FXML
|
updateService.setOnFailed(this::updateFailed);
|
||||||
public void checkNow() {
|
|
||||||
updateChecker.checkForUpdatesNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
public void visitDownloadsPage() {
|
|
||||||
application.getHostServices().showDocument(downloadsUri);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -108,38 +111,104 @@ public class UpdatesPreferencesController implements FxController {
|
|||||||
environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
|
environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void startWork() {
|
||||||
|
if (worker.get().equals(updateChecker)) {
|
||||||
|
updateChecker.checkForUpdatesNow();
|
||||||
|
} else if (!unlockedVaults.isEmpty()) {
|
||||||
|
LOG.warn("Cannot start update due to unlocked vaults.");
|
||||||
|
} else if (worker.get().equals(updateService)) {
|
||||||
|
LOG.info("User started update to version {}", updateChecker.getUpdate().version());
|
||||||
|
updateService.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkFailed(WorkerStateEvent workerStateEvent) {
|
||||||
|
assert workerStateEvent.getSource() == updateChecker;
|
||||||
|
LOG.error("Update check failed.", updateChecker.getException());
|
||||||
|
errorMessage.set(resourceBundle.getString("preferences.updates.checkFailed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSucceeded(WorkerStateEvent workerStateEvent) {
|
||||||
|
assert workerStateEvent.getSource() == updateService;
|
||||||
|
var lastStep = updateService.getValue();
|
||||||
|
if (lastStep == UpdateStep.EXIT) {
|
||||||
|
// Record that this version attempted an update, so next launch can choose fallback if needed
|
||||||
|
settings.lastUpdateAttemptedByVersion.set(environment.getAppVersionWithBuildNumber());
|
||||||
|
settings.saveNow();
|
||||||
|
LOG.info("Exiting app to update...");
|
||||||
|
Platform.exit();
|
||||||
|
} else if (lastStep == UpdateStep.RETRY) {
|
||||||
|
updateService.reset();
|
||||||
|
} else {
|
||||||
|
LOG.info("Update succeeded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFailed(WorkerStateEvent workerStateEvent) {
|
||||||
|
assert workerStateEvent.getSource() == updateService;
|
||||||
|
LOG.error("Update failed.", updateService.getException());
|
||||||
|
updateService.reset();
|
||||||
|
errorMessage.set(resourceBundle.getString("preferences.updates.updateFailed"));
|
||||||
|
// try fallback mechanism:
|
||||||
|
updateChecker.recheckWithFallbackMechanism();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void lockAllGracefully() {
|
||||||
|
vaultService.lockAll(unlockedVaults, false);
|
||||||
|
}
|
||||||
|
|
||||||
/* Observable Properties */
|
/* Observable Properties */
|
||||||
|
|
||||||
public ObjectBinding<ContentDisplay> checkForUpdatesButtonStateProperty() {
|
public UpdateChecker getUpdateChecker() {
|
||||||
return checkForUpdatesButtonState;
|
return updateChecker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentDisplay getCheckForUpdatesButtonState() {
|
public ObjectBinding<Worker<?>> workerProperty() {
|
||||||
return checkForUpdatesButtonState.get();
|
return worker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlyStringProperty latestVersionProperty() {
|
public Worker<?> getWorker() {
|
||||||
return latestVersion;
|
return worker.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLatestVersion() {
|
public BooleanExpression runningProperty() {
|
||||||
return latestVersion.get();
|
return running;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCurrentVersion() {
|
public boolean isRunning() {
|
||||||
return currentVersion;
|
return updateChecker.getState() == Worker.State.RUNNING || updateService.getState() == Worker.State.RUNNING;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringBinding lastUpdateCheckMessageProperty() {
|
public StringBinding updateButtonTitleProperty() {
|
||||||
return lastUpdateCheckMessage;
|
return updateButtonTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLastUpdateCheckMessage() {
|
public String getUpdateButtonTitle() {
|
||||||
Instant lastCheck = lastSuccessfulUpdateCheck.getValue();
|
if (worker.get() == updateChecker) {
|
||||||
if (lastCheck != null && !lastCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
|
return resourceBundle.getString("preferences.updates.checkNowBtn");
|
||||||
return formatter.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault()));
|
|
||||||
} else {
|
} else {
|
||||||
return "-";
|
return switch (updateService.getState()) {
|
||||||
|
case READY -> updateChecker.getUpdate().updateMechanism().getName();
|
||||||
|
case SCHEDULED, RUNNING -> updateService.getMessage();
|
||||||
|
case SUCCEEDED -> resourceBundle.getString("generic.button.done");
|
||||||
|
case FAILED, CANCELLED -> "failed"; // should never be visible
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectBinding<ContentDisplay> updateButtonStateProperty() {
|
||||||
|
return updateButtonState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentDisplay getUpdateButtonState() {
|
||||||
|
if (updateService.isRunning()) { // isRunning() covers RUNNING and SCHEDULED states
|
||||||
|
return ContentDisplay.BOTTOM;
|
||||||
|
} else if (updateChecker.getState() == Worker.State.RUNNING) {
|
||||||
|
return ContentDisplay.LEFT;
|
||||||
|
} else {
|
||||||
|
return ContentDisplay.TEXT_ONLY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +217,7 @@ public class UpdatesPreferencesController implements FxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getTimeDifferenceMessage() {
|
public String getTimeDifferenceMessage() {
|
||||||
var lastSuccessCheck = lastSuccessfulUpdateCheck.getValue();
|
var lastSuccessCheck = updateChecker.getLastSuccessfulUpdateCheck();
|
||||||
var duration = Duration.between(lastSuccessCheck, Instant.now());
|
var duration = Duration.between(lastSuccessCheck, Instant.now());
|
||||||
var hours = duration.toHours();
|
var hours = duration.toHours();
|
||||||
if (lastSuccessCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
|
if (lastSuccessCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
|
||||||
@@ -162,6 +231,44 @@ public class UpdatesPreferencesController implements FxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public StringBinding lastUpdateCheckMessageProperty() {
|
||||||
|
return lastUpdateCheckMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastUpdateCheckMessage() {
|
||||||
|
Instant lastCheck = updateChecker.getLastSuccessfulUpdateCheck();
|
||||||
|
if (lastCheck != null && !lastCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
|
||||||
|
return FORMATTER.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault()));
|
||||||
|
} else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return errorMessage.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyStringProperty errorMessageProperty() {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isProhibitUpdateWhileUnlocked() {
|
||||||
|
// If the result of the last update check was from the fallback mechanism, we don't need to show the warning
|
||||||
|
return !unlockedVaults.isEmpty() && !FallbackUpdateInfo.class.isInstance(updateChecker.getUpdate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding prohibitUpdateWhileUnlockedProperty() {
|
||||||
|
return prohibitUpdateWhileUnlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUpdateButtonDisabled() {
|
||||||
|
return updateButtonDisabled.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding updateButtonDisabledProperty() {
|
||||||
|
return updateButtonDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
public BooleanProperty upToDateLabelVisibleProperty() {
|
public BooleanProperty upToDateLabelVisibleProperty() {
|
||||||
return upToDateLabelVisible;
|
return upToDateLabelVisible;
|
||||||
}
|
}
|
||||||
@@ -170,20 +277,4 @@ public class UpdatesPreferencesController implements FxController {
|
|||||||
return upToDateLabelVisible.get();
|
return upToDateLabelVisible.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanBinding updateAvailableProperty() {
|
|
||||||
return updateAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isUpdateAvailable() {
|
|
||||||
return updateAvailable.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public BooleanBinding checkFailedProperty() {
|
|
||||||
return checkFailed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCheckFailed() {
|
|
||||||
return checkFailed.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package org.cryptomator.ui.updatereminder;
|
|||||||
|
|
||||||
import org.cryptomator.common.settings.Settings;
|
import org.cryptomator.common.settings.Settings;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
import org.cryptomator.updater.UpdateChecker;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package org.cryptomator.updater;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLParameters;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Authenticator;
|
||||||
|
import java.net.CookieHandler;
|
||||||
|
import java.net.ProxySelector;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
abstract class DelegatingHttpClient extends HttpClient {
|
||||||
|
|
||||||
|
private final HttpClient delegate;
|
||||||
|
|
||||||
|
public DelegatingHttpClient(HttpClient delegate) {
|
||||||
|
this.delegate = Objects.requireNonNull(delegate, "delegate must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<CookieHandler> cookieHandler() {
|
||||||
|
return delegate.cookieHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Duration> connectTimeout() {
|
||||||
|
return delegate.connectTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Redirect followRedirects() {
|
||||||
|
return delegate.followRedirects();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ProxySelector> proxy() {
|
||||||
|
return delegate.proxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SSLContext sslContext() {
|
||||||
|
return delegate.sslContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SSLParameters sslParameters() {
|
||||||
|
return delegate.sslParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Authenticator> authenticator() {
|
||||||
|
return delegate.authenticator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Version version() {
|
||||||
|
return delegate.version();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Executor> executor() {
|
||||||
|
return delegate.executor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException {
|
||||||
|
return delegate.send(request, responseBodyHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
|
||||||
|
return delegate.sendAsync(request, responseBodyHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
|
||||||
|
return delegate.sendAsync(request, responseBodyHandler, pushPromiseHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package org.cryptomator.updater;
|
||||||
|
|
||||||
|
import org.cryptomator.integrations.update.UpdateInfo;
|
||||||
|
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||||
|
|
||||||
|
public record FallbackUpdateInfo(String version, UpdateMechanism<FallbackUpdateInfo> updateMechanism) implements UpdateInfo<FallbackUpdateInfo> {}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package org.cryptomator.updater;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
|
import org.cryptomator.common.Environment;
|
||||||
|
import org.cryptomator.integrations.common.LocalizedDisplayName;
|
||||||
|
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||||
|
import org.cryptomator.integrations.update.UpdateStep;
|
||||||
|
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
@FxApplicationScoped
|
||||||
|
@LocalizedDisplayName(bundle = "i18n.strings", key = "preferences.updates.visitDownloadPage")
|
||||||
|
public class FallbackUpdateMechanism implements UpdateMechanism<FallbackUpdateInfo> {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(FallbackUpdateMechanism.class);
|
||||||
|
private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version";
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
|
||||||
|
+ "?utm_source=cryptomator-desktop" //
|
||||||
|
+ "&utm_medium=update-notification&" //
|
||||||
|
+ "utm_campaign=app-update-%s";
|
||||||
|
|
||||||
|
private final Application app;
|
||||||
|
private final Environment env;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public FallbackUpdateMechanism(Application app, Environment env) {
|
||||||
|
this.app = app;
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FallbackUpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) {
|
||||||
|
try {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build();
|
||||||
|
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
throw new RuntimeException("Failed to fetch release: " + response.statusCode());
|
||||||
|
}
|
||||||
|
var release = MAPPER.readValue(response.body(), LatestVersion.class);
|
||||||
|
var updateVersion = release.versionForCurrentOS();
|
||||||
|
if (UpdateMechanism.isUpdateAvailable(updateVersion, currentVersion)) {
|
||||||
|
return new FallbackUpdateInfo(updateVersion, this);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
LOG.warn("Update check interrupted", e);
|
||||||
|
return null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warn("Update check failed", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UpdateStep firstStep(FallbackUpdateInfo updateInfo) {
|
||||||
|
return UpdateStep.of("Go to download page", this::openDownloadPage); // TODO localize
|
||||||
|
}
|
||||||
|
|
||||||
|
private UpdateStep openDownloadPage() {
|
||||||
|
var downloadUrl = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(env.getAppVersion(), StandardCharsets.US_ASCII));
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
app.getHostServices().showDocument(downloadUrl);
|
||||||
|
});
|
||||||
|
return UpdateStep.RETRY; // allow running this "update mechanism" as many times as the user wants
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record LatestVersion(
|
||||||
|
@JsonProperty("mac") String macVersion,
|
||||||
|
@JsonProperty("win") String winVersion,
|
||||||
|
@JsonProperty("linux") String linuxVersion
|
||||||
|
) {
|
||||||
|
public String versionForCurrentOS() {
|
||||||
|
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||||
|
return macVersion;
|
||||||
|
} else if (SystemUtils.IS_OS_WINDOWS) {
|
||||||
|
return winVersion;
|
||||||
|
} else if (SystemUtils.IS_OS_LINUX) {
|
||||||
|
return linuxVersion;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unsupported operating system");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package org.cryptomator.updater;
|
||||||
|
|
||||||
|
import org.cryptomator.common.Environment;
|
||||||
|
import org.cryptomator.common.settings.Settings;
|
||||||
|
import org.cryptomator.integrations.update.UpdateFailedException;
|
||||||
|
import org.cryptomator.integrations.update.UpdateInfo;
|
||||||
|
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||||
|
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.binding.ObjectBinding;
|
||||||
|
import javafx.beans.binding.StringExpression;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.concurrent.ScheduledService;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
@FxApplicationScoped
|
||||||
|
public class UpdateChecker extends ScheduledService<UpdateInfo<?>> {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class);
|
||||||
|
private static final Duration AUTO_CHECK_DELAY = Duration.seconds(5);
|
||||||
|
private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3);
|
||||||
|
private static final Duration DISABLED_UPDATE_CHECK_INTERVAL = Duration.hours(100000); // Duration.INDEFINITE leads to overflows...
|
||||||
|
|
||||||
|
public enum UpdateCheckState {
|
||||||
|
NOT_CHECKED,
|
||||||
|
IS_CHECKING,
|
||||||
|
CHECK_SUCCESSFUL,
|
||||||
|
CHECK_FAILED
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Environment env;
|
||||||
|
private final Settings settings;
|
||||||
|
private final ObjectProperty<Instant> lastSuccessfulUpdateCheck;
|
||||||
|
private final ObjectProperty<UpdateInfo<?>> update = new SimpleObjectProperty<>();
|
||||||
|
private final StringExpression latestVersion = StringExpression.stringExpression(update.map(UpdateInfo::version));
|
||||||
|
private final BooleanBinding updateAvailable = update.isNotNull();
|
||||||
|
private final ObjectBinding<UpdateCheckState> updateState = Bindings.createObjectBinding(this::getUpdateCheckState, stateProperty());
|
||||||
|
private final BooleanBinding checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, updateState);
|
||||||
|
private final UpdateMechanism<?> fallbackUpdateMechanism;
|
||||||
|
private UpdateMechanism<?> updateMechanism;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UpdateChecker(Settings settings, //
|
||||||
|
Environment env,
|
||||||
|
FallbackUpdateMechanism fallbackUpdateMechanism) {
|
||||||
|
this.env = env;
|
||||||
|
this.settings = settings;
|
||||||
|
this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck;
|
||||||
|
this.fallbackUpdateMechanism = fallbackUpdateMechanism;
|
||||||
|
|
||||||
|
// Prefer the safer fallback mechanism if the last update attempt was already made by this app version
|
||||||
|
var currentVersion = env.getAppVersionWithBuildNumber();
|
||||||
|
var lastAttemptedBy = settings.lastUpdateAttemptedByVersion.get();
|
||||||
|
if (currentVersion != null && currentVersion.equals(lastAttemptedBy)) {
|
||||||
|
this.updateMechanism = fallbackUpdateMechanism; // immediately use fallback mechanism
|
||||||
|
} else {
|
||||||
|
this.updateMechanism = UpdateMechanism.get().orElse(fallbackUpdateMechanism);
|
||||||
|
}
|
||||||
|
|
||||||
|
setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
||||||
|
periodProperty().bind(Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void automaticallyCheckForUpdatesIfEnabled() {
|
||||||
|
if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) {
|
||||||
|
startCheckingForUpdates(AUTO_CHECK_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recheckWithFallbackMechanism() {
|
||||||
|
if (updateMechanism == fallbackUpdateMechanism) {
|
||||||
|
return; // already using fallback mechanism
|
||||||
|
}
|
||||||
|
updateMechanism = fallbackUpdateMechanism;
|
||||||
|
checkForUpdatesNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkForUpdatesNow() {
|
||||||
|
startCheckingForUpdates(Duration.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCheckingForUpdates(Duration initialDelay) {
|
||||||
|
cancel();
|
||||||
|
reset();
|
||||||
|
setDelay(initialDelay);
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void succeeded() {
|
||||||
|
var updateInfo = getValue();
|
||||||
|
super.succeeded(); // this will nil the value property!
|
||||||
|
lastSuccessfulUpdateCheck.set(Instant.now());
|
||||||
|
if (updateInfo != null) {
|
||||||
|
LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), updateInfo.version());
|
||||||
|
update.set(updateInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<UpdateInfo<?>> createTask() {
|
||||||
|
return new UpdateCheckTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Observable Properties */
|
||||||
|
|
||||||
|
public UpdateInfo<?> getUpdate() {
|
||||||
|
return update.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<UpdateInfo<?>> updateProperty() {
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLatestVersion() {
|
||||||
|
return latestVersion.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringExpression latestVersionProperty() {
|
||||||
|
return latestVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUpdateAvailable() {
|
||||||
|
return updateAvailable.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding updateAvailableProperty() {
|
||||||
|
return updateAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCheckFailed() {
|
||||||
|
return checkFailed.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding checkFailedProperty() {
|
||||||
|
return checkFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getLastSuccessfulUpdateCheck() {
|
||||||
|
return lastSuccessfulUpdateCheck.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<Instant> lastSuccessfulUpdateCheckProperty() {
|
||||||
|
return lastSuccessfulUpdateCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectBinding<UpdateCheckState> updateCheckStateProperty() {
|
||||||
|
return updateState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UpdateCheckState getUpdateCheckState() {
|
||||||
|
return switch (getState()) {
|
||||||
|
case READY -> UpdateCheckState.NOT_CHECKED;
|
||||||
|
case SCHEDULED, RUNNING -> UpdateCheckState.IS_CHECKING;
|
||||||
|
case SUCCEEDED -> UpdateCheckState.CHECK_SUCCESSFUL;
|
||||||
|
case FAILED, CANCELLED -> UpdateCheckState.CHECK_FAILED;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentVersion() {
|
||||||
|
return env.getAppVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UpdateCheckTask extends Task<UpdateInfo<?>> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected UpdateInfo<?> call() {
|
||||||
|
try (var httpClient = new UpdateCheckerHttpClient(env)) {
|
||||||
|
var result = updateMechanism.checkForUpdate(env.getAppVersion(), httpClient);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (UpdateFailedException e) {
|
||||||
|
LOG.error("Update check using {} failed.", updateMechanism.getClass(), e);
|
||||||
|
}
|
||||||
|
if (updateMechanism == fallbackUpdateMechanism) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
LOG.debug("Trying fallback update check...");
|
||||||
|
try (var httpClient = new UpdateCheckerHttpClient(env)) {
|
||||||
|
return fallbackUpdateMechanism.checkForUpdate(env.getAppVersion(), httpClient);
|
||||||
|
} catch (UpdateFailedException e) {
|
||||||
|
LOG.error("Fallback update check failed.", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package org.cryptomator.updater;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
|
import org.cryptomator.common.Environment;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.ProxySelector;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class UpdateCheckerHttpClient extends DelegatingHttpClient {
|
||||||
|
|
||||||
|
private final String userAgent;
|
||||||
|
|
||||||
|
public UpdateCheckerHttpClient(Environment env) {
|
||||||
|
var delegate = HttpClient.newBuilder() //
|
||||||
|
.followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes
|
||||||
|
.proxy(ProxySelector.getDefault()).build();
|
||||||
|
super(delegate);
|
||||||
|
this.userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", env.getAppVersion(), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException {
|
||||||
|
return super.send(decorateRequest(request), responseBodyHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
|
||||||
|
return super.sendAsync(decorateRequest(request), responseBodyHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
|
||||||
|
return super.sendAsync(decorateRequest(request), responseBodyHandler, pushPromiseHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequest decorateRequest(HttpRequest request) {
|
||||||
|
return HttpRequest.newBuilder(request, (_, _) -> true) //
|
||||||
|
.header("User-Agent", this.userAgent) //
|
||||||
|
.timeout(Duration.ofSeconds(10)) //
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package org.cryptomator.updater;
|
||||||
|
|
||||||
|
import org.cryptomator.integrations.update.UpdateInfo;
|
||||||
|
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||||
|
import org.cryptomator.integrations.update.UpdateStep;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.concurrent.Service;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that performs all update steps provided by the given {@link UpdateMechanism} in sequence.
|
||||||
|
*/
|
||||||
|
public class UpdateService extends Service<UpdateStep> {
|
||||||
|
|
||||||
|
private final BooleanBinding updateFailed = Bindings.equal(State.FAILED, stateProperty());
|
||||||
|
|
||||||
|
private ObservableValue<UpdateInfo<?>> updateInfo;
|
||||||
|
|
||||||
|
public UpdateService(ObservableValue<UpdateInfo<?>> updateInfo) {
|
||||||
|
setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
||||||
|
this.updateInfo = updateInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<UpdateStep> createTask() {
|
||||||
|
return new RunAllStepsTask(updateInfo.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RunAllStepsTask extends Task<UpdateStep> {
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private final UpdateInfo updateInfo;
|
||||||
|
|
||||||
|
public RunAllStepsTask(UpdateInfo<?> updateInfo) {
|
||||||
|
this.updateInfo = Objects.requireNonNull(updateInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected UpdateStep call() throws IOException {
|
||||||
|
try {
|
||||||
|
UpdateStep step = updateInfo.useToPrepareFirstStep();
|
||||||
|
UpdateStep lastStep;
|
||||||
|
do {
|
||||||
|
step.start();
|
||||||
|
observeAndWaitFor(step);
|
||||||
|
lastStep = step;
|
||||||
|
step = step.nextStep();
|
||||||
|
} while (step != null);
|
||||||
|
return lastStep;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new InterruptedIOException("Update interrupted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void observeAndWaitFor(UpdateStep step) throws InterruptedException {
|
||||||
|
do {
|
||||||
|
updateProgress(step.preparationProgress(), 1.0);
|
||||||
|
updateMessage(step.description());
|
||||||
|
} while (!step.await(100, TimeUnit.MILLISECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Observable Properties */
|
||||||
|
|
||||||
|
public boolean isUpdateFailed() {
|
||||||
|
return updateFailed.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding updateFailedProperty() {
|
||||||
|
return updateFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,55 +3,73 @@
|
|||||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||||
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
|
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
|
||||||
<?import org.cryptomator.ui.controls.FormattedLabel?>
|
<?import org.cryptomator.ui.controls.FormattedLabel?>
|
||||||
<?import org.cryptomator.ui.controls.FormattedString?>
|
|
||||||
<?import javafx.geometry.Insets?>
|
<?import javafx.geometry.Insets?>
|
||||||
<?import javafx.scene.control.Button?>
|
<?import javafx.scene.control.Button?>
|
||||||
<?import javafx.scene.control.CheckBox?>
|
<?import javafx.scene.control.CheckBox?>
|
||||||
<?import javafx.scene.control.Hyperlink?>
|
<?import javafx.scene.control.Hyperlink?>
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.control.ProgressBar?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
|
||||||
<?import javafx.scene.control.Tooltip?>
|
<?import javafx.scene.control.Tooltip?>
|
||||||
<?import javafx.scene.text.TextFlow?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.text.Text?>
|
<?import javafx.scene.text.Text?>
|
||||||
|
<?import javafx.scene.text.TextFlow?>
|
||||||
<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.preferences.UpdatesPreferencesController"
|
fx:controller="org.cryptomator.ui.preferences.UpdatesPreferencesController"
|
||||||
spacing="12">
|
spacing="12">
|
||||||
<fx:define>
|
|
||||||
<FormattedString fx:id="linkLabel" format="%preferences.updates.updateAvailable" arg1="${controller.latestVersion}"/>
|
|
||||||
</fx:define>
|
|
||||||
<padding>
|
<padding>
|
||||||
<Insets topRightBottomLeft="24"/>
|
<Insets topRightBottomLeft="24"/>
|
||||||
</padding>
|
</padding>
|
||||||
<FormattedLabel format="%preferences.updates.currentVersion" arg1="${controller.currentVersion}" textAlignment="CENTER" wrapText="true"/>
|
<FormattedLabel format="%preferences.updates.currentVersion" arg1="${controller.updateChecker.currentVersion}" textAlignment="CENTER" wrapText="true"/>
|
||||||
|
|
||||||
<CheckBox fx:id="checkForUpdatesCheckbox" text="%preferences.updates.autoUpdateCheck"/>
|
<CheckBox fx:id="checkForUpdatesCheckbox" text="%preferences.updates.autoUpdateCheck"/>
|
||||||
|
|
||||||
<VBox alignment="CENTER" spacing="12">
|
<VBox alignment="CENTER" spacing="12">
|
||||||
<Button text="%preferences.updates.checkNowBtn" defaultButton="true" onAction="#checkNow" contentDisplay="${controller.checkForUpdatesButtonState}">
|
<FormattedLabel format="%preferences.updates.updateAvailable" arg1="${controller.updateChecker.latestVersion}" textAlignment="CENTER" wrapText="true" visible="${controller.updateChecker.updateAvailable}"/>
|
||||||
|
|
||||||
|
<Button text="${controller.updateButtonTitle}" defaultButton="true" onAction="#startWork" disable="${controller.updateButtonDisabled}" contentDisplay="${controller.updateButtonState}">
|
||||||
<graphic>
|
<graphic>
|
||||||
<FontAwesome5Spinner glyphSize="12"/>
|
<VBox spacing="5" alignment="CENTER">
|
||||||
|
<ProgressBar maxWidth="200"
|
||||||
|
maxHeight="12"
|
||||||
|
visible="${controller.running && controller.worker.progress != -1.0}"
|
||||||
|
managed="${controller.running && controller.worker.progress != -1.0}"
|
||||||
|
progress="${controller.worker.progress}"/>
|
||||||
|
<FontAwesome5Spinner glyphSize="12"
|
||||||
|
visible="${controller.running && controller.worker.progress == -1.0}"
|
||||||
|
managed="${controller.running && controller.worker.progress == -1.0}"/>
|
||||||
|
</VBox>
|
||||||
</graphic>
|
</graphic>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<TextFlow styleClass="text-flow" textAlignment="CENTER" visible="${controller.checkFailed}" managed="${controller.checkFailed}">
|
<TextFlow styleClass="text-flow" textAlignment="CENTER" visible="${controller.prohibitUpdateWhileUnlocked}" managed="${controller.prohibitUpdateWhileUnlocked}">
|
||||||
|
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-primary" glyph="LOCK_OPEN"/>
|
||||||
|
<Text text=" "/>
|
||||||
|
<Text text="%preferences.updates.prohibitedDueToUnlockedVaults.1"/>
|
||||||
|
<Text text=" "/>
|
||||||
|
<Hyperlink styleClass="hyperlink-underline" text="%preferences.updates.prohibitedDueToUnlockedVaults.2" onAction="#lockAllGracefully"/>
|
||||||
|
<Text text=" "/>
|
||||||
|
<Text text="%preferences.updates.prohibitedDueToUnlockedVaults.3"/>
|
||||||
|
</TextFlow>
|
||||||
|
|
||||||
|
<TextFlow styleClass="text-flow" textAlignment="CENTER" visible="${!controller.errorMessage.empty}" managed="${!controller.errorMessage.empty}">
|
||||||
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-orange" glyph="EXCLAMATION_TRIANGLE"/>
|
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-orange" glyph="EXCLAMATION_TRIANGLE"/>
|
||||||
<Text text=" "/>
|
<Text text=" "/>
|
||||||
<Text text="%preferences.updates.checkFailed"/>
|
<Text text="${controller.errorMessage}"/>
|
||||||
<Text text=" "/>
|
<Text text=" "/>
|
||||||
<Hyperlink styleClass="hyperlink-underline" text="%preferences.general.debugDirectory" onAction="#showLogfileDirectory"/>
|
<Hyperlink styleClass="hyperlink-underline" text="%preferences.general.debugDirectory" onAction="#showLogfileDirectory"/>
|
||||||
</TextFlow>
|
</TextFlow>
|
||||||
<FormattedLabel format="%preferences.updates.lastUpdateCheck" arg1="${controller.timeDifferenceMessage}" textAlignment="CENTER" wrapText="true">
|
|
||||||
|
<FormattedLabel format="%preferences.updates.lastUpdateCheck" arg1="${controller.timeDifferenceMessage}" textAlignment="CENTER" wrapText="true" visible="${!controller.updateChecker.updateAvailable}" managed="${!controller.updateChecker.updateAvailable}">
|
||||||
<tooltip>
|
<tooltip>
|
||||||
<Tooltip text="${controller.lastUpdateCheckMessage}" showDelay="10ms"/>
|
<Tooltip text="${controller.lastUpdateCheckMessage}" showDelay="10ms"/>
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</FormattedLabel>
|
</FormattedLabel>
|
||||||
|
|
||||||
<Label text="%preferences.updates.upToDate" visible="${controller.upToDateLabelVisible}" managed="${controller.upToDateLabelVisible}">
|
<Label text="%preferences.updates.upToDate" visible="${controller.upToDateLabelVisible}" managed="${controller.upToDateLabelVisible}">
|
||||||
<graphic>
|
<graphic>
|
||||||
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-primary" glyph="CHECK"/>
|
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-primary" glyph="CHECK"/>
|
||||||
</graphic>
|
</graphic>
|
||||||
</Label>
|
</Label>
|
||||||
<Hyperlink text="${linkLabel.value}" onAction="#visitDownloadsPage" textAlignment="CENTER" wrapText="true" styleClass="hyperlink-underline" visible="${controller.updateAvailable}" managed="${controller.updateAvailable}"/>
|
|
||||||
</VBox>
|
</VBox>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|||||||
@@ -330,8 +330,13 @@ preferences.updates.lastUpdateCheck.never=never
|
|||||||
preferences.updates.lastUpdateCheck.recently=recently
|
preferences.updates.lastUpdateCheck.recently=recently
|
||||||
preferences.updates.lastUpdateCheck.daysAgo=%s days ago
|
preferences.updates.lastUpdateCheck.daysAgo=%s days ago
|
||||||
preferences.updates.lastUpdateCheck.hoursAgo=%s hours ago
|
preferences.updates.lastUpdateCheck.hoursAgo=%s hours ago
|
||||||
|
preferences.updates.prohibitedDueToUnlockedVaults.1=Please
|
||||||
|
preferences.updates.prohibitedDueToUnlockedVaults.2=lock your vaults
|
||||||
|
preferences.updates.prohibitedDueToUnlockedVaults.3=to install the update.
|
||||||
preferences.updates.checkFailed=Looking for updates failed. Please check your internet connection or try again later.
|
preferences.updates.checkFailed=Looking for updates failed. Please check your internet connection or try again later.
|
||||||
|
preferences.updates.updateFailed=Update failed. Please install the update manually.
|
||||||
preferences.updates.upToDate=Cryptomator is up-to-date.
|
preferences.updates.upToDate=Cryptomator is up-to-date.
|
||||||
|
preferences.updates.visitDownloadPage=Visit Download Page
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
preferences.contribute=Support Us
|
preferences.contribute=Support Us
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
|
||||||
* All rights reserved.
|
|
||||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
|
||||||
*
|
|
||||||
* Contributors:
|
|
||||||
* Sebastian Stenzel - initial API and implementation
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.common;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
public class SemVerComparatorTest {
|
|
||||||
|
|
||||||
private final Comparator<String> semVerComparator = new SemVerComparator();
|
|
||||||
|
|
||||||
// equal versions
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void compareEqualVersions() {
|
|
||||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4")));
|
|
||||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha")));
|
|
||||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4+20170101", "1.23.4+20171231")));
|
|
||||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha+20170101", "1.23.4-alpha+20171231")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// newer versions in first argument
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void compareHigherToLowerVersions() {
|
|
||||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.5", "1.23.4")));
|
|
||||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.24.4", "1.23.4")));
|
|
||||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23")));
|
|
||||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-SNAPSHOT")));
|
|
||||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-56.78")));
|
|
||||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-beta", "1.23.4-alpha")));
|
|
||||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-alpha.1", "1.23.4-alpha")));
|
|
||||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-56.79", "1.23.4-56.78")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// newer versions in second argument
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void compareLowerToHigherVersions() {
|
|
||||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.5")));
|
|
||||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.24.4")));
|
|
||||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23", "1.23.4")));
|
|
||||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-SNAPSHOT", "1.23.4")));
|
|
||||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4")));
|
|
||||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-beta")));
|
|
||||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha.1")));
|
|
||||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4-56.79")));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -9,31 +9,28 @@ import org.cryptomator.common.Environment;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public class SettingsTest {
|
public class SettingsTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAutoSave() {
|
public void testAutoSave() {
|
||||||
Environment env = Mockito.mock(Environment.class);
|
Environment env = Mockito.mock(Environment.class);
|
||||||
@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
|
SettingsProvider provider = Mockito.mock(SettingsProvider.class);
|
||||||
|
|
||||||
Settings settings = Settings.create(env);
|
Settings settings = Settings.create(provider, env);
|
||||||
settings.setSaveCmd(changeListener);
|
|
||||||
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
||||||
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
|
Mockito.verify(provider, Mockito.times(0)).scheduleSave(settings);
|
||||||
|
|
||||||
// first change (to property):
|
// first change (to property):
|
||||||
settings.port.set(42428);
|
settings.port.set(42428);
|
||||||
Mockito.verify(changeListener, Mockito.times(1)).accept(settings);
|
Mockito.verify(provider, Mockito.times(1)).scheduleSave(settings);
|
||||||
|
|
||||||
// second change (to list):
|
// second change (to list):
|
||||||
settings.directories.add(vaultSettings);
|
settings.directories.add(vaultSettings);
|
||||||
Mockito.verify(changeListener, Mockito.times(2)).accept(settings);
|
Mockito.verify(provider, Mockito.times(2)).scheduleSave(settings);
|
||||||
|
|
||||||
// third change (to property of list item):
|
// third change (to property of list item):
|
||||||
vaultSettings.displayName.set("asd");
|
vaultSettings.displayName.set("asd");
|
||||||
Mockito.verify(changeListener, Mockito.times(3)).accept(settings);
|
Mockito.verify(provider, Mockito.times(3)).scheduleSave(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user