mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-23 00:14:40 +00:00
Compare commits
126
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad240eb18b | ||
|
|
8105dbc236 | ||
|
|
ceed70c314 | ||
|
|
eddf586b81 | ||
|
|
0d7ddd3189 | ||
|
|
491cd5e550 | ||
|
|
93d95ecd93 | ||
|
|
05a41a625c | ||
|
|
2aff02a186 | ||
|
|
4b7de11750 | ||
|
|
aaf921afcf | ||
|
|
7718445f7c | ||
|
|
0fa27f66c9 | ||
|
|
111c5ae2de | ||
|
|
8841bb1c6a | ||
|
|
8ca7e34d8f | ||
|
|
0fa6f0d915 | ||
|
|
4ffd8f4467 | ||
|
|
bb0c1b54c1 | ||
|
|
8eb91f6471 | ||
|
|
eb20fd7278 | ||
|
|
d836798361 | ||
|
|
0cd6eb8b5e | ||
|
|
0b2107309b | ||
|
|
309881b339 | ||
|
|
532cdf30ac | ||
|
|
d746e9e215 | ||
|
|
408c80744a | ||
|
|
bf890d2165 | ||
|
|
eb90a7fb33 | ||
|
|
a47814f38c | ||
|
|
8b157fd935 | ||
|
|
5e582f666c | ||
|
|
bebe3064ee | ||
|
|
bbf8e1d1e5 | ||
|
|
98d2442deb | ||
|
|
cac5c86dab | ||
|
|
5a0145800d | ||
|
|
f4b56e5e51 | ||
|
|
7cfb63ed48 | ||
|
|
2521cd2bfb | ||
|
|
1e861ac187 | ||
|
|
a4c85be61c | ||
|
|
5b9e70da33 | ||
|
|
b83a3c27c6 | ||
|
|
5c2954de03 | ||
|
|
8a834fe71c | ||
|
|
4ff2905723 | ||
|
|
538e01b1e5 | ||
|
|
3135be9178 | ||
|
|
2c456f5323 | ||
|
|
9d464e3a4f | ||
|
|
83199eb1de | ||
|
|
5772ffec17 | ||
|
|
f3415277d7 | ||
|
|
8e9a001fde | ||
|
|
5e229395f1 | ||
|
|
18dd9770f2 | ||
|
|
185c5a4f5d | ||
|
|
285ddac219 | ||
|
|
9e631a78f2 | ||
|
|
7fe9049266 | ||
|
|
39ab9ad63f | ||
|
|
b4528f825e | ||
|
|
3d679c73e7 | ||
|
|
04bdb2d8c4 | ||
|
|
f32d24c1c0 | ||
|
|
f4f093bb42 | ||
|
|
3f06460667 | ||
|
|
617f1bf2c9 | ||
|
|
02ad38f871 | ||
|
|
7a2944cbea | ||
|
|
4c47204f92 | ||
|
|
b6d0823c24 | ||
|
|
7f4776a995 | ||
|
|
9d2b31cd56 | ||
|
|
f052395a7f | ||
|
|
57be83b38d | ||
|
|
c2cd4f5bbf | ||
|
|
d3d57312ba | ||
|
|
593a64c9bd | ||
|
|
19dc4fb6ff | ||
|
|
81f45012f3 | ||
|
|
f91cc2374c | ||
|
|
48298bb161 | ||
|
|
7a1cd9026c | ||
|
|
754e53d8db | ||
|
|
c36f1bc8d0 | ||
|
|
8a243a01aa | ||
|
|
9e4006cc89 | ||
|
|
26b69beb87 | ||
|
|
f95bf87a4b | ||
|
|
e854c7d189 | ||
|
|
8a434dcd96 | ||
|
|
6b7324723e | ||
|
|
0bdcb2b3be | ||
|
|
c3931d9d29 | ||
|
|
307825a339 | ||
|
|
093f0e8c94 | ||
|
|
c938c42c00 | ||
|
|
cee22e34db | ||
|
|
18300ee67f | ||
|
|
59560193ee | ||
|
|
a6b31e19b9 | ||
|
|
8a44115234 | ||
|
|
43a1f00bea | ||
|
|
3e458060bc | ||
|
|
4bb5a3f10d | ||
|
|
5797108dfe | ||
|
|
e1ff552f08 | ||
|
|
f3d4494d91 | ||
|
|
bdc1cc3f05 | ||
|
|
cdcd43a805 | ||
|
|
f5bd23de51 | ||
|
|
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:
|
||||||
@@ -44,9 +44,9 @@ jobs:
|
|||||||
openjfx-url: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_linux-aarch64_bin-jmods.zip'
|
openjfx-url: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_linux-aarch64_bin-jmods.zip'
|
||||||
openjfx-sha: '9ad4ca7b769ca4ee6419f1e99143dd6ff812f8be4fddb46a7d7cacbeea148af4'
|
openjfx-sha: '9ad4ca7b769ca4ee6419f1e99143dd6ff812f8be4fddb46a7d7cacbeea148af4'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||||
with:
|
with:
|
||||||
distribution: ${{ env.JAVA_DIST }}
|
distribution: ${{ env.JAVA_DIST }}
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
@@ -175,7 +175,7 @@ jobs:
|
|||||||
gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage
|
gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage
|
||||||
gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage.zsync
|
gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage.zsync
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: appimage-${{ matrix.appimage-suffix }}
|
name: appimage-${{ matrix.appimage-suffix }}
|
||||||
path: |
|
path: |
|
||||||
@@ -185,7 +185,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
- name: Publish AppImage on GitHub Releases
|
- name: Publish AppImage on GitHub Releases
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.event.action == 'published'
|
if: startsWith(github.ref, 'refs/tags/') && github.event.action == 'published'
|
||||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
||||||
@@ -201,7 +201,7 @@ jobs:
|
|||||||
if: github.event_name == 'release' && needs.get-version.outputs.versionType == 'stable'
|
if: github.event_name == 'release' && needs.get-version.outputs.versionType == 'stable'
|
||||||
steps:
|
steps:
|
||||||
- name: Download AppImages
|
- name: Download AppImages
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
path: downloads/
|
path: downloads/
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -212,7 +212,7 @@ jobs:
|
|||||||
echo "x64-sha256sum=${X64_SHA256}" >> "$GITHUB_OUTPUT"
|
echo "x64-sha256sum=${X64_SHA256}" >> "$GITHUB_OUTPUT"
|
||||||
AARCH64_SHA256=$(sha256sum downloads/cryptomator-*-aarch64.AppImage | cut -d ' ' -f1)
|
AARCH64_SHA256=$(sha256sum downloads/cryptomator-*-aarch64.AppImage | cut -d ' ' -f1)
|
||||||
echo "aarch64-sha256sum=${AARCH64_SHA256}" >> "$GITHUB_OUTPUT"
|
echo "aarch64-sha256sum=${AARCH64_SHA256}" >> "$GITHUB_OUTPUT"
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
repository: 'cryptomator/aur-bin'
|
repository: 'cryptomator/aur-bin'
|
||||||
token: ${{ secrets.CRYPTOBOT_PR_TOKEN }}
|
token: ${{ secrets.CRYPTOBOT_PR_TOKEN }}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
AUR_PR_URL: tbd
|
AUR_PR_URL: tbd
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
repository: 'cryptomator/aur'
|
repository: 'cryptomator/aur'
|
||||||
token: ${{ secrets.CRYPTOBOT_PR_TOKEN }}
|
token: ${{ secrets.CRYPTOBOT_PR_TOKEN }}
|
||||||
|
|||||||
@@ -7,6 +7,16 @@ on:
|
|||||||
description: "Url to the file to upload"
|
description: "Url to the file to upload"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
avast:
|
||||||
|
description: "Upload to Avast"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
kaspersky:
|
||||||
|
description: "Upload to Kaspersky"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
url:
|
url:
|
||||||
@@ -39,9 +49,9 @@ jobs:
|
|||||||
url="${INPUT_URL}"
|
url="${INPUT_URL}"
|
||||||
echo "fileName=${url##*/}" >> $GITHUB_OUTPUT
|
echo "fileName=${url##*/}" >> $GITHUB_OUTPUT
|
||||||
- name: Download file
|
- name: Download file
|
||||||
run: curl --remote-name ${INPUT_URL} -L -o ${{steps.extractName.outputs.fileName}}
|
run: curl "${INPUT_URL}" -L -o "${{steps.extractName.outputs.fileName}}" --fail-with-body
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.extractName.outputs.fileName }}
|
name: ${{ steps.extractName.outputs.fileName }}
|
||||||
path: ${{ steps.extractName.outputs.fileName }}
|
path: ${{ steps.extractName.outputs.fileName }}
|
||||||
@@ -50,10 +60,10 @@ jobs:
|
|||||||
name: Anti Virus Allowlisting Kaspersky
|
name: Anti Virus Allowlisting Kaspersky
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: download-file
|
needs: download-file
|
||||||
if: github.event_name == 'workflow_call' || inputs.kaspersky
|
if: inputs.kaspersky
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.download-file.outputs.fileName }}
|
name: ${{ needs.download-file.outputs.fileName }}
|
||||||
path: upload
|
path: upload
|
||||||
@@ -70,10 +80,10 @@ jobs:
|
|||||||
name: Anti Virus Allowlisting Avast
|
name: Anti Virus Allowlisting Avast
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: download-file
|
needs: download-file
|
||||||
if: github.event_name == 'workflow_call' || inputs.avast
|
if: inputs.avast
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.download-file.outputs.fileName }}
|
name: ${{ needs.download-file.outputs.fileName }}
|
||||||
path: upload
|
path: upload
|
||||||
|
|||||||
+19
-14
@@ -11,7 +11,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_DIST: 'temurin'
|
JAVA_DIST: 'temurin'
|
||||||
JAVA_VERSION: 24
|
JAVA_VERSION: 25
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -22,8 +22,8 @@ jobs:
|
|||||||
name: Compile and Test
|
name: Compile and Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||||
with:
|
with:
|
||||||
distribution: ${{ env.JAVA_DIST }}
|
distribution: ${{ env.JAVA_DIST }}
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
@@ -49,28 +49,28 @@ jobs:
|
|||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
- name: Draft a release
|
- name: Draft a release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
discussion_category_name: releases
|
discussion_category_name: releases
|
||||||
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
body: |-
|
body: |-
|
||||||
:construction: Work in Progress
|
> [!NOTE]
|
||||||
|
> 🚧 Work in Progress 🚧
|
||||||
|
>
|
||||||
|
> Please be patient, the [builds are still running](https://github.com/cryptomator/cryptomator/actions). Binary packages can be found here in a few moments.
|
||||||
|
|
||||||
|
<!--REPLACE with auto-generated release notes (see below)
|
||||||
### What's New 🎉
|
### What's New 🎉
|
||||||
|
|
||||||
### Bugfixes 🐛
|
### Bugfixes 🐛
|
||||||
|
|
||||||
### Other Changes 📎
|
### Other Changes 📎
|
||||||
|
END REPLACE-->
|
||||||
|
|
||||||
|
Feel free to also read our [CHANGELOG.md](https://github.com/cryptomator/cryptomator/blob/develop/CHANGELOG.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
TODO FULL CHANGELOG
|
|
||||||
|
|
||||||
📜 List of closed issues is available [here](TODO)
|
|
||||||
|
|
||||||
---
|
|
||||||
⏳ Please be patient, the builds are still [running](https://github.com/cryptomator/cryptomator/actions). New versions of Cryptomator can be found here in a few moments. ⏳
|
|
||||||
|
|
||||||
<!-- Don't forget to include the
|
<!-- Don't forget to include the
|
||||||
💾 SHA-256 checksums of release artifacts:
|
💾 SHA-256 checksums of release artifacts:
|
||||||
@@ -78,4 +78,9 @@ jobs:
|
|||||||
```
|
```
|
||||||
-->
|
-->
|
||||||
|
|
||||||
As usual, the GPG signatures can be checked using [our public key `5811 7AFA 1F85 B3EE C154 677D 615D 449F E6E6 A235`](https://gist.github.com/cryptobot/211111cf092037490275f39d408f461a).
|
> [!TIP]
|
||||||
|
> You can verify the GPG signature of all assets using our public key: [`5811 7AFA 1F85 B3EE C154 677D 615D 449F E6E6 A235`](https://gist.github.com/cryptobot/211111cf092037490275f39d408f461a).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Auto-Generated Release Notes: -->
|
||||||
|
|||||||
@@ -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,10 +23,10 @@ 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@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JDK_MAJOR_VERSION}}
|
java-version: ${{ env.JDK_MAJOR_VERSION}}
|
||||||
distribution: ${{ env.JDK_VENDOR }}
|
distribution: ${{ env.JDK_VENDOR }}
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -44,7 +43,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
INPUT_PPAVER: ${{ inputs.ppaver }}
|
INPUT_PPAVER: ${{ inputs.ppaver }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- id: deb-version
|
- id: deb-version
|
||||||
name: Determine deb-version
|
name: Determine deb-version
|
||||||
run: |
|
run: |
|
||||||
@@ -55,11 +54,13 @@ 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@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||||
with:
|
with:
|
||||||
distribution: ${{ env.JAVA_DIST }}
|
distribution: ${{ env.JAVA_DIST }}
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
@@ -142,7 +143,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator_*_amd64.deb
|
gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator_*_amd64.deb
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: linux-deb-package
|
name: linux-deb-package
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
INTERVAL: 900
|
INTERVAL: 900
|
||||||
JSON_DATA: ${{ steps.get-stats.outputs.result }}
|
JSON_DATA: ${{ steps.get-stats.outputs.result }}
|
||||||
- name: Upload Results
|
- name: Upload Results
|
||||||
uses: fjogeleit/http-request-action@1297c6fc63a79b147d1676540a3fd9d2e37817c5 # v1.16.5
|
uses: fjogeleit/http-request-action@c0b95d02a088b47c1f2f4db04fd8af8bd19eee54 # v1.16.6
|
||||||
with:
|
with:
|
||||||
url: 'https://graphite-us-central1.grafana.net/metrics'
|
url: 'https://graphite-us-central1.grafana.net/metrics'
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
FLATHUB_PR_URL: tbd
|
FLATHUB_PR_URL: tbd
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
repository: 'flathub/org.cryptomator.Cryptomator'
|
repository: 'flathub/org.cryptomator.Cryptomator'
|
||||||
token: ${{ secrets.CRYPTOBOT_PR_TOKEN }}
|
token: ${{ secrets.CRYPTOBOT_PR_TOKEN }}
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -35,11 +35,11 @@ jobs:
|
|||||||
revNum: ${{ steps.versions.outputs.revNum }}
|
revNum: ${{ steps.versions.outputs.revNum }}
|
||||||
type: ${{ steps.versions.outputs.type}}
|
type: ${{ steps.versions.outputs.type}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||||
with:
|
with:
|
||||||
distribution: ${{ env.JAVA_DIST }}
|
distribution: ${{ env.JAVA_DIST }}
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_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:
|
||||||
@@ -47,9 +47,9 @@ jobs:
|
|||||||
openjfx-url: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_osx-x64_bin-jmods.zip'
|
openjfx-url: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_osx-x64_bin-jmods.zip'
|
||||||
openjfx-sha: '0eba73fb28a24c845175d16fa2f8c081c936ce6de1be9b79eb6119fa32e53d52'
|
openjfx-sha: '0eba73fb28a24c845175d16fa2f8c081c936ce6de1be9b79eb6119fa32e53d52'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||||
with:
|
with:
|
||||||
distribution: ${{ env.JAVA_DIST }}
|
distribution: ${{ env.JAVA_DIST }}
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_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
|
||||||
@@ -282,7 +283,7 @@ jobs:
|
|||||||
run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db
|
run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: dmg-${{ matrix.output-suffix }}
|
name: dmg-${{ matrix.output-suffix }}
|
||||||
path: |
|
path: |
|
||||||
@@ -291,7 +292,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
- name: Publish dmg on GitHub Releases
|
- name: Publish dmg on GitHub Releases
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.event.action == 'published'
|
if: startsWith(github.ref, 'refs/tags/') && github.event.action == 'published'
|
||||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -45,9 +45,9 @@ jobs:
|
|||||||
openjfx-url: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_osx-aarch64_bin-jmods.zip'
|
openjfx-url: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_osx-aarch64_bin-jmods.zip'
|
||||||
openjfx-sha: '13f8c0513c40c95881479fbcf0465a29a60217393fb0656f5e4eab78a9442fba'
|
openjfx-sha: '13f8c0513c40c95881479fbcf0465a29a60217393fb0656f5e4eab78a9442fba'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||||
with:
|
with:
|
||||||
distribution: ${{ env.JAVA_DIST }}
|
distribution: ${{ env.JAVA_DIST }}
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_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
|
||||||
@@ -281,7 +282,7 @@ jobs:
|
|||||||
run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db
|
run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: dmg-${{ matrix.output-suffix }}
|
name: dmg-${{ matrix.output-suffix }}
|
||||||
path: |
|
path: |
|
||||||
@@ -290,7 +291,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
- name: Publish dmg on GitHub Releases
|
- name: Publish dmg on GitHub Releases
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.event.action == 'published'
|
if: startsWith(github.ref, 'refs/tags/') && github.event.action == 'published'
|
||||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||||
with:
|
with:
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: 0
|
days-before-close: 0
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
|
GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
|
||||||
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
|
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
|
||||||
- name: Publish asc on GitHub Releases
|
- name: Publish asc on GitHub Releases
|
||||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_DIST: 'temurin'
|
JAVA_DIST: 'temurin'
|
||||||
JAVA_VERSION: 24
|
JAVA_VERSION: 25
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -16,8 +16,8 @@ jobs:
|
|||||||
name: Compile and Test
|
name: Compile and Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||||
with:
|
with:
|
||||||
distribution: ${{ env.JAVA_DIST }}
|
distribution: ${{ env.JAVA_DIST }}
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
|
|||||||
@@ -12,16 +12,16 @@ defaults:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
JAVA_DIST: 'temurin'
|
JAVA_DIST: 'temurin'
|
||||||
JAVA_VERSION: 23
|
JAVA_VERSION: 25
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-preconditions:
|
check-preconditions:
|
||||||
name: Validate commits pushed to release/hotfix branch to fulfill release requirements
|
name: Validate commits pushed to release/hotfix branch to fulfill release requirements
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||||
with:
|
with:
|
||||||
distribution: ${{ env.JAVA_DIST }}
|
distribution: ${{ env.JAVA_DIST }}
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
@@ -43,7 +43,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
- name: Validate release in org.cryptomator.Cryptomator.metainfo.xml file
|
- name: Validate release in org.cryptomator.Cryptomator.metainfo.xml file
|
||||||
if: ${{ ! (contains(github.event.head_commit.message, '[skip metadata check]') || contains(github.event.head_commit.message, '[metadata check skip]')) }}
|
|
||||||
run: |
|
run: |
|
||||||
if ! grep -q "<release date=\".*\" version=\"${{ steps.validate-pom-version.outputs.semVerStr }}\">" dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml; then
|
if ! grep -q "<release date=\".*\" version=\"${{ steps.validate-pom-version.outputs.semVerStr }}\">" dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml; then
|
||||||
echo "Release not set in dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml"
|
echo "Release not set in dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||||
with:
|
with:
|
||||||
days-before-stale: 365
|
days-before-stale: 365
|
||||||
days-before-close: 90
|
days-before-close: 90
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||||
with:
|
with:
|
||||||
distribution: ${{ matrix.java-dist }}
|
distribution: ${{ matrix.java-dist }}
|
||||||
java-version: ${{ matrix.java-version }}
|
java-version: ${{ matrix.java-version }}
|
||||||
@@ -200,7 +200,7 @@ jobs:
|
|||||||
client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
|
client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||||
- name: Sign DLLs with Actalis CodeSigner
|
- name: Sign DLLs with Actalis CodeSigner
|
||||||
if: inputs.sign || github.event_name == 'release'
|
if: inputs.sign || github.event_name == 'release'
|
||||||
uses: skymatic/workflows/.github/actions/win-sign-action@450e322ff2214d0be0b079b63343c894f3ef735f # no specific version
|
uses: skymatic/workflows/.github/actions/win-sign-action@1074588008ae3326a2221ea451783280518f0366 # no specific version
|
||||||
with:
|
with:
|
||||||
base-dir: 'appdir'
|
base-dir: 'appdir'
|
||||||
file-extensions: 'dll,exe,ps1'
|
file-extensions: 'dll,exe,ps1'
|
||||||
@@ -277,7 +277,7 @@ jobs:
|
|||||||
GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
|
GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
|
||||||
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
|
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: msi-${{ matrix.arch }}
|
name: msi-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -299,21 +299,21 @@ jobs:
|
|||||||
java-version: '24.0.1+9'
|
java-version: '24.0.1+9'
|
||||||
java-package: 'jdk'
|
java-package: 'jdk'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Install wix and extensions
|
- name: Install wix and extensions
|
||||||
run: |
|
run: |
|
||||||
dotnet tool install --global wix --version 6.0.0
|
dotnet tool install --global wix --version 6.0.0
|
||||||
wix.exe extension add WixToolset.BootstrapperApplications.wixext/6.0.0 --global
|
wix.exe extension add WixToolset.BootstrapperApplications.wixext/6.0.0 --global
|
||||||
wix.exe extension add WixToolset.Util.wixext/6.0.0 --global
|
wix.exe extension add WixToolset.Util.wixext/6.0.0 --global
|
||||||
- name: Download .msi
|
- name: Download .msi
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: msi-${{ matrix.arch }}
|
name: msi-${{ matrix.arch }}
|
||||||
path: dist/win/bundle/resources
|
path: dist/win/bundle/resources
|
||||||
- name: Strip version info from msi file name
|
- name: Strip version info from msi file name
|
||||||
run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi
|
run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||||
with:
|
with:
|
||||||
distribution: ${{ matrix.java-dist }}
|
distribution: ${{ matrix.java-dist }}
|
||||||
java-version: ${{ matrix.java-version }}
|
java-version: ${{ matrix.java-version }}
|
||||||
@@ -376,7 +376,7 @@ jobs:
|
|||||||
client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
|
client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||||
- name: Sign burn engine with Actalis CodeSigner
|
- name: Sign burn engine with Actalis CodeSigner
|
||||||
if: inputs.sign || github.event_name == 'release'
|
if: inputs.sign || github.event_name == 'release'
|
||||||
uses: skymatic/workflows/.github/actions/win-sign-action@450e322ff2214d0be0b079b63343c894f3ef735f # no specific version
|
uses: skymatic/workflows/.github/actions/win-sign-action@1074588008ae3326a2221ea451783280518f0366 # no specific version
|
||||||
with:
|
with:
|
||||||
base-dir: 'tmp'
|
base-dir: 'tmp'
|
||||||
file-extensions: 'exe'
|
file-extensions: 'exe'
|
||||||
@@ -400,7 +400,7 @@ jobs:
|
|||||||
client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
|
client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||||
- name: Sign installer with Actalis CodeSigner
|
- name: Sign installer with Actalis CodeSigner
|
||||||
if: inputs.sign || github.event_name == 'release'
|
if: inputs.sign || github.event_name == 'release'
|
||||||
uses: skymatic/workflows/.github/actions/win-sign-action@450e322ff2214d0be0b079b63343c894f3ef735f # no specific version
|
uses: skymatic/workflows/.github/actions/win-sign-action@1074588008ae3326a2221ea451783280518f0366 # no specific version
|
||||||
with:
|
with:
|
||||||
base-dir: 'installer'
|
base-dir: 'installer'
|
||||||
file-extensions: 'exe'
|
file-extensions: 'exe'
|
||||||
@@ -418,7 +418,7 @@ jobs:
|
|||||||
GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
|
GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
|
||||||
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
|
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: exe-${{ matrix.executable-suffix }}
|
name: exe-${{ matrix.executable-suffix }}
|
||||||
path: |
|
path: |
|
||||||
@@ -436,12 +436,12 @@ jobs:
|
|||||||
download-url-exe-x64: ${{ fromJSON(steps.publish.outputs.assets)[2].browser_download_url }}
|
download-url-exe-x64: ${{ fromJSON(steps.publish.outputs.assets)[2].browser_download_url }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download installers
|
- name: Download installers
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Publish installers on GitHub Releases
|
- name: Publish installers on GitHub Releases
|
||||||
id: publish
|
id: publish
|
||||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
||||||
|
|||||||
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,22 @@
|
|||||||
|
# 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
|
||||||
|
* Self-Update Mechanism ([#3948](https://github.com/cryptomator/cryptomator/pull/3948))
|
||||||
|
* Implemented `.dmg` update mechanism
|
||||||
|
* Implemented Flatpak update mechanism
|
||||||
|
* App notifications ([#4069](https://github.com/cryptomator/cryptomator/pull/4069))
|
||||||
|
* Mark files in-use for Hub vaults ([#4078](https://github.com/cryptomator/cryptomator/pull/4078))
|
||||||
|
* Accessibility labels for GUI elements ([#4064](https://github.com/cryptomator/cryptomator/issues/4064), [#4066](https://github.com/cryptomator/cryptomator/pull/4066), [#4055](https://github.com/cryptomator/cryptomator/issues/4055))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Built using JDK 25 ([#4031](https://github.com/cryptomator/cryptomator/issues/4031))
|
||||||
|
* Modernized Template for GitHub Releases
|
||||||
Vendored
+1
@@ -115,6 +115,7 @@ cp ../common/org.cryptomator.Cryptomator.tray-unlocked.svg Cryptomator.AppDir/us
|
|||||||
cp ../common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop
|
cp ../common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop
|
||||||
cp ../common/org.cryptomator.Cryptomator.metainfo.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.metainfo.xml
|
cp ../common/org.cryptomator.Cryptomator.metainfo.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.metainfo.xml
|
||||||
cp ../common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml
|
cp ../common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml
|
||||||
|
cp ../common/application-vnd.cryptomator.encrypted.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.encrypted.xml
|
||||||
ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg
|
ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg
|
||||||
ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon
|
ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon
|
||||||
ln -s usr/share/applications/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/org.cryptomator.Cryptomator.desktop
|
ln -s usr/share/applications/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/org.cryptomator.Cryptomator.desktop
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||||
|
<mime-type type="application/vnd.cryptomator.encrypted">
|
||||||
|
<comment>Cryptomator Encrypted Data</comment>
|
||||||
|
<glob pattern="*.c9r"/>
|
||||||
|
<glob pattern="*.c9s"/>
|
||||||
|
<glob pattern="*.c9u"/>
|
||||||
|
</mime-type>
|
||||||
|
</mime-info>
|
||||||
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
|
||||||
|
|||||||
+2
-1
@@ -6,4 +6,5 @@ common/org.cryptomator.Cryptomator.tray-unlocked.svg usr/share/icons/hicolor/sca
|
|||||||
common/org.cryptomator.Cryptomator256.png usr/share/icons/hicolor/256x256/apps
|
common/org.cryptomator.Cryptomator256.png usr/share/icons/hicolor/256x256/apps
|
||||||
common/org.cryptomator.Cryptomator512.png usr/share/icons/hicolor/512x512/apps
|
common/org.cryptomator.Cryptomator512.png usr/share/icons/hicolor/512x512/apps
|
||||||
common/org.cryptomator.Cryptomator.metainfo.xml usr/share/metainfo
|
common/org.cryptomator.Cryptomator.metainfo.xml usr/share/metainfo
|
||||||
common/application-vnd.cryptomator.vault.xml usr/share/mime/packages
|
common/application-vnd.cryptomator.vault.xml usr/share/mime/packages
|
||||||
|
common/application-vnd.cryptomator.encrypted.xml usr/share/mime/packages
|
||||||
Vendored
+1
@@ -25,6 +25,7 @@ case "$1" in
|
|||||||
fi
|
fi
|
||||||
xdg-desktop-menu install --novendor /usr/share/applications/org.cryptomator.Cryptomator.desktop
|
xdg-desktop-menu install --novendor /usr/share/applications/org.cryptomator.Cryptomator.desktop
|
||||||
xdg-mime install /usr/share/mime/packages/application-vnd.cryptomator.vault.xml
|
xdg-mime install /usr/share/mime/packages/application-vnd.cryptomator.vault.xml
|
||||||
|
xdg-mime install /usr/share/mime/packages/application-vnd.cryptomator.encrypted.xml
|
||||||
;;
|
;;
|
||||||
|
|
||||||
abort-upgrade|abort-remove|abort-deconfigure)
|
abort-upgrade|abort-remove|abort-deconfigure)
|
||||||
|
|||||||
Vendored
+1
@@ -23,6 +23,7 @@ case "$1" in
|
|||||||
|
|
||||||
xdg-desktop-menu uninstall --novendor /usr/share/applications/org.cryptomator.Cryptomator.desktop
|
xdg-desktop-menu uninstall --novendor /usr/share/applications/org.cryptomator.Cryptomator.desktop
|
||||||
xdg-mime uninstall /usr/share/mime/packages/application-vnd.cryptomator.vault.xml
|
xdg-mime uninstall /usr/share/mime/packages/application-vnd.cryptomator.vault.xml
|
||||||
|
xdg-mime uninstall /usr/share/mime/packages/application-vnd.cryptomator.encrypted.xml
|
||||||
;;
|
;;
|
||||||
|
|
||||||
failed-upgrade)
|
failed-upgrade)
|
||||||
|
|||||||
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
|
||||||
|
|||||||
Vendored
+1
@@ -105,6 +105,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>c9r</string>
|
<string>c9r</string>
|
||||||
<string>c9s</string>
|
<string>c9s</string>
|
||||||
|
<string>c9u</string>
|
||||||
</array>
|
</array>
|
||||||
<key>public.mime-type</key>
|
<key>public.mime-type</key>
|
||||||
<array>
|
<array>
|
||||||
|
|||||||
Vendored
+1
@@ -94,6 +94,7 @@
|
|||||||
<ns0:MIME ContentType="$(var.ProgIdContentType)" Default="yes"/>
|
<ns0:MIME ContentType="$(var.ProgIdContentType)" Default="yes"/>
|
||||||
</ns0:Extension>
|
</ns0:Extension>
|
||||||
<ns0:Extension Id="c9s" Advertise="no" ContentType="$(var.ProgIdContentType)"/>
|
<ns0:Extension Id="c9s" Advertise="no" ContentType="$(var.ProgIdContentType)"/>
|
||||||
|
<ns0:Extension Id="c9u" Advertise="no" ContentType="$(var.ProgIdContentType)"/>
|
||||||
</ns0:ProgId>
|
</ns0:ProgId>
|
||||||
</ns0:Component>
|
</ns0:Component>
|
||||||
</ns0:DirectoryRef>
|
</ns0:DirectoryRef>
|
||||||
|
|||||||
+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.1</version>
|
<version>1.19.0</version>
|
||||||
<name>Cryptomator Desktop App</name>
|
<name>Cryptomator Desktop App</name>
|
||||||
|
|
||||||
<organization>
|
<organization>
|
||||||
@@ -26,18 +26,18 @@
|
|||||||
|
|
||||||
<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) -->
|
||||||
<nonModularGroupIds>org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents</nonModularGroupIds>
|
<nonModularGroupIds>org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents</nonModularGroupIds>
|
||||||
|
|
||||||
<!-- cryptomator dependencies -->
|
<!-- cryptomator dependencies -->
|
||||||
<cryptomator.cryptofs.version>2.9.0</cryptomator.cryptofs.version>
|
<cryptomator.cryptofs.version>2.10.0-beta2</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-beta3</cryptomator.integrations.mac.version>
|
||||||
<cryptomator.integrations.linux.version>1.6.1</cryptomator.integrations.linux.version>
|
<cryptomator.integrations.linux.version>1.7.0-beta3</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>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import ch.qos.logback.classic.spi.Configurator;
|
import ch.qos.logback.classic.spi.Configurator;
|
||||||
import org.cryptomator.networking.SSLContextWithPKCS12TrustStore;
|
|
||||||
import org.cryptomator.common.locationpresets.DropboxLinuxLocationPresetsProvider;
|
import org.cryptomator.common.locationpresets.DropboxLinuxLocationPresetsProvider;
|
||||||
import org.cryptomator.common.locationpresets.DropboxMacLocationPresetsProvider;
|
import org.cryptomator.common.locationpresets.DropboxMacLocationPresetsProvider;
|
||||||
import org.cryptomator.common.locationpresets.DropboxWindowsLocationPresetsProvider;
|
import org.cryptomator.common.locationpresets.DropboxWindowsLocationPresetsProvider;
|
||||||
@@ -14,11 +13,12 @@ import org.cryptomator.common.locationpresets.OneDriveLinuxLocationPresetsProvid
|
|||||||
import org.cryptomator.common.locationpresets.OneDriveMacLocationPresetsProvider;
|
import org.cryptomator.common.locationpresets.OneDriveMacLocationPresetsProvider;
|
||||||
import org.cryptomator.common.locationpresets.OneDriveWindowsLocationPresetsProvider;
|
import org.cryptomator.common.locationpresets.OneDriveWindowsLocationPresetsProvider;
|
||||||
import org.cryptomator.common.locationpresets.PCloudLocationPresetsProvider;
|
import org.cryptomator.common.locationpresets.PCloudLocationPresetsProvider;
|
||||||
import org.cryptomator.networking.SSLContextWithMacKeychain;
|
|
||||||
import org.cryptomator.networking.SSLContextProvider;
|
|
||||||
import org.cryptomator.networking.SSLContextWithWindowsCertStore;
|
|
||||||
import org.cryptomator.integrations.tray.TrayMenuController;
|
import org.cryptomator.integrations.tray.TrayMenuController;
|
||||||
import org.cryptomator.logging.LogbackConfiguratorFactory;
|
import org.cryptomator.logging.LogbackConfiguratorFactory;
|
||||||
|
import org.cryptomator.networking.SSLContextProvider;
|
||||||
|
import org.cryptomator.networking.SSLContextWithMacKeychain;
|
||||||
|
import org.cryptomator.networking.SSLContextWithPKCS12TrustStore;
|
||||||
|
import org.cryptomator.networking.SSLContextWithWindowsCertStore;
|
||||||
import org.cryptomator.ui.traymenu.AwtTrayMenuController;
|
import org.cryptomator.ui.traymenu.AwtTrayMenuController;
|
||||||
|
|
||||||
open module org.cryptomator.desktop {
|
open module org.cryptomator.desktop {
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -13,5 +13,7 @@ public interface Constants {
|
|||||||
String CRYPTOMATOR_FILENAME_GLOB = "*.cryptomator";
|
String CRYPTOMATOR_FILENAME_GLOB = "*.cryptomator";
|
||||||
URI DEFAULT_KEY_ID = URI.create(MasterkeyFileLoadingStrategy.SCHEME + ":" + MASTERKEY_FILENAME);
|
URI DEFAULT_KEY_ID = URI.create(MasterkeyFileLoadingStrategy.SCHEME + ":" + MASTERKEY_FILENAME);
|
||||||
byte[] PEPPER = new byte[0];
|
byte[] PEPPER = new byte[0];
|
||||||
|
// Separator used to concatenate Hub username and device name in the filesystem owner identifier.
|
||||||
|
String HUB_USER_DEVICE_SEPARATOR = "&";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,160 +0,0 @@
|
|||||||
package org.cryptomator.common;
|
|
||||||
|
|
||||||
import org.cryptomator.cryptofs.event.BrokenDirFileEvent;
|
|
||||||
import org.cryptomator.cryptofs.event.BrokenFileNodeEvent;
|
|
||||||
import org.cryptomator.cryptofs.event.ConflictResolutionFailedEvent;
|
|
||||||
import org.cryptomator.cryptofs.event.ConflictResolvedEvent;
|
|
||||||
import org.cryptomator.cryptofs.event.DecryptionFailedEvent;
|
|
||||||
import org.cryptomator.cryptofs.event.FilesystemEvent;
|
|
||||||
import org.cryptomator.event.VaultEvent;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import javafx.beans.InvalidationListener;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.MapChangeListener;
|
|
||||||
import javafx.collections.ObservableMap;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map containing {@link VaultEvent}s.
|
|
||||||
* The map is keyed by the ciphertext path of the affected resource _and_ the {@link FilesystemEvent}s class in order to group same events
|
|
||||||
* <p>
|
|
||||||
* Use {@link EventMap#put(VaultEvent)} to add an element and {@link EventMap#remove(VaultEvent)} to remove it.
|
|
||||||
* <p>
|
|
||||||
* The map is size restricted to {@value MAX_SIZE} elements. If a _new_ element (i.e. not already present) is added, the least recently added is removed.
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
public class EventMap implements ObservableMap<EventMap.EventKey, VaultEvent> {
|
|
||||||
|
|
||||||
private static final int MAX_SIZE = 300;
|
|
||||||
|
|
||||||
public record EventKey(Path ciphertextPath, Class<? extends FilesystemEvent> c) {}
|
|
||||||
|
|
||||||
private final ObservableMap<EventMap.EventKey, VaultEvent> delegate;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public EventMap() {
|
|
||||||
delegate = FXCollections.observableHashMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addListener(MapChangeListener<? super EventKey, ? super VaultEvent> mapChangeListener) {
|
|
||||||
delegate.addListener(mapChangeListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeListener(MapChangeListener<? super EventKey, ? super VaultEvent> mapChangeListener) {
|
|
||||||
delegate.removeListener(mapChangeListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int size() {
|
|
||||||
return delegate.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return delegate.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean containsKey(Object key) {
|
|
||||||
return delegate.containsKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean containsValue(Object value) {
|
|
||||||
return delegate.containsValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public VaultEvent get(Object key) {
|
|
||||||
return delegate.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable VaultEvent put(EventKey key, VaultEvent value) {
|
|
||||||
return delegate.put(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public VaultEvent remove(Object key) {
|
|
||||||
return delegate.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void putAll(@NotNull Map<? extends EventKey, ? extends VaultEvent> m) {
|
|
||||||
delegate.putAll(m);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clear() {
|
|
||||||
delegate.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Set<EventKey> keySet() {
|
|
||||||
return delegate.keySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Collection<VaultEvent> values() {
|
|
||||||
return delegate.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Set<Entry<EventKey, VaultEvent>> entrySet() {
|
|
||||||
return delegate.entrySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addListener(InvalidationListener invalidationListener) {
|
|
||||||
delegate.addListener(invalidationListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeListener(InvalidationListener invalidationListener) {
|
|
||||||
delegate.removeListener(invalidationListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void put(VaultEvent e) {
|
|
||||||
//compute key
|
|
||||||
var key = computeKey(e.actualEvent());
|
|
||||||
//if-else
|
|
||||||
var nullOrEntry = delegate.get(key);
|
|
||||||
if (nullOrEntry == null) {
|
|
||||||
if (size() == MAX_SIZE) {
|
|
||||||
delegate.entrySet().stream() //
|
|
||||||
.min(Comparator.comparing(entry -> entry.getValue().actualEvent().getTimestamp())) //
|
|
||||||
.ifPresent(oldestEntry -> delegate.remove(oldestEntry.getKey()));
|
|
||||||
}
|
|
||||||
delegate.put(key, e);
|
|
||||||
} else {
|
|
||||||
delegate.put(key, nullOrEntry.incrementCount(e.actualEvent()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized VaultEvent remove(VaultEvent similar) {
|
|
||||||
//compute key
|
|
||||||
var key = computeKey(similar.actualEvent());
|
|
||||||
return this.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private EventKey computeKey(FilesystemEvent e) {
|
|
||||||
var p = switch (e) {
|
|
||||||
case DecryptionFailedEvent(_, Path ciphertextPath, _) -> ciphertextPath;
|
|
||||||
case ConflictResolvedEvent(_, _, _, _, Path resolvedCiphertext) -> resolvedCiphertext;
|
|
||||||
case ConflictResolutionFailedEvent(_, _, Path conflictingCiphertext, _) -> conflictingCiphertext;
|
|
||||||
case BrokenDirFileEvent(_, Path ciphertext) -> ciphertext;
|
|
||||||
case BrokenFileNodeEvent(_, _, Path ciphertext) -> ciphertext;
|
|
||||||
};
|
|
||||||
return new EventKey(p, e.getClass());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.cryptomator.common;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface marking a class to be used in {@link org.cryptomator.cryptofs.CryptoFileSystemProperties.Builder#withOwnerGetter(Supplier)}.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface FilesystemOwnerSupplier {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filesystem owner.
|
||||||
|
*
|
||||||
|
* @return the filesystem owner
|
||||||
|
*/
|
||||||
|
String getOwner();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -167,6 +167,7 @@ public class Mounter {
|
|||||||
usedMountServices.add(mountService);
|
usedMountServices.add(mountService);
|
||||||
|
|
||||||
var builder = mountService.forFileSystem(cryptoFsRoot);
|
var builder = mountService.forFileSystem(cryptoFsRoot);
|
||||||
|
LOG.debug("Using mount service {} for mounting vault {}", mountService.getClass().getName(), vaultSettings.displayName);
|
||||||
var internal = new SettledMounter(mountService, builder, vaultSettings); // FIXME: no need for an inner class
|
var internal = new SettledMounter(mountService, builder, vaultSettings); // FIXME: no need for an inner class
|
||||||
var cleanup = internal.prepare();
|
var cleanup = internal.prepare();
|
||||||
return new MountHandle(builder.mount(), mountService.hasCapability(UNMOUNT_FORCED), cleanup);
|
return new MountHandle(builder.mount(), mountService.hasCapability(UNMOUNT_FORCED), cleanup);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ package org.cryptomator.common.vaults;
|
|||||||
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
import org.cryptomator.common.Constants;
|
import org.cryptomator.common.Constants;
|
||||||
import org.cryptomator.event.FileSystemEventAggregator;
|
import org.cryptomator.common.FilesystemOwnerSupplier;
|
||||||
import org.cryptomator.common.mount.Mounter;
|
import org.cryptomator.common.mount.Mounter;
|
||||||
import org.cryptomator.common.settings.Settings;
|
import org.cryptomator.common.settings.Settings;
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
@@ -23,6 +23,8 @@ import org.cryptomator.cryptofs.event.FilesystemEvent;
|
|||||||
import org.cryptomator.cryptolib.api.CryptoException;
|
import org.cryptomator.cryptolib.api.CryptoException;
|
||||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||||
|
import org.cryptomator.event.FileSystemEventAggregator;
|
||||||
|
import org.cryptomator.event.NotificationManager;
|
||||||
import org.cryptomator.integrations.mount.MountFailedException;
|
import org.cryptomator.integrations.mount.MountFailedException;
|
||||||
import org.cryptomator.integrations.mount.Mountpoint;
|
import org.cryptomator.integrations.mount.Mountpoint;
|
||||||
import org.cryptomator.integrations.mount.UnmountFailedException;
|
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||||
@@ -78,6 +80,7 @@ public class Vault {
|
|||||||
private final Mounter mounter;
|
private final Mounter mounter;
|
||||||
private final Settings settings;
|
private final Settings settings;
|
||||||
private final FileSystemEventAggregator fileSystemEventAggregator;
|
private final FileSystemEventAggregator fileSystemEventAggregator;
|
||||||
|
private final NotificationManager notificationManager;
|
||||||
private final BooleanProperty showingStats;
|
private final BooleanProperty showingStats;
|
||||||
|
|
||||||
private final AtomicReference<Mounter.MountHandle> mountHandle = new AtomicReference<>(null);
|
private final AtomicReference<Mounter.MountHandle> mountHandle = new AtomicReference<>(null);
|
||||||
@@ -90,7 +93,8 @@ public class Vault {
|
|||||||
@Named("lastKnownException") ObjectProperty<Exception> lastKnownException, //
|
@Named("lastKnownException") ObjectProperty<Exception> lastKnownException, //
|
||||||
VaultStats stats, //
|
VaultStats stats, //
|
||||||
Mounter mounter, Settings settings, //
|
Mounter mounter, Settings settings, //
|
||||||
FileSystemEventAggregator fileSystemEventAggregator) {
|
FileSystemEventAggregator fileSystemEventAggregator, //
|
||||||
|
NotificationManager notificationManager) {
|
||||||
this.vaultSettings = vaultSettings;
|
this.vaultSettings = vaultSettings;
|
||||||
this.configCache = configCache;
|
this.configCache = configCache;
|
||||||
this.cryptoFileSystem = cryptoFileSystem;
|
this.cryptoFileSystem = cryptoFileSystem;
|
||||||
@@ -109,6 +113,7 @@ public class Vault {
|
|||||||
this.mounter = mounter;
|
this.mounter = mounter;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.fileSystemEventAggregator = fileSystemEventAggregator;
|
this.fileSystemEventAggregator = fileSystemEventAggregator;
|
||||||
|
this.notificationManager = notificationManager;
|
||||||
this.showingStats = new SimpleBooleanProperty(false);
|
this.showingStats = new SimpleBooleanProperty(false);
|
||||||
this.quickAccessEntry = new AtomicReference<>(null);
|
this.quickAccessEntry = new AtomicReference<>(null);
|
||||||
}
|
}
|
||||||
@@ -145,14 +150,17 @@ public class Vault {
|
|||||||
LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength.get());
|
LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
var fsPropsBuilder = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||||
.withKeyLoader(keyLoader) //
|
.withKeyLoader(keyLoader) //
|
||||||
.withFlags(flags) //
|
.withFlags(flags) //
|
||||||
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength.get()) //
|
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength.get()) //
|
||||||
.withVaultConfigFilename(Constants.VAULTCONFIG_FILENAME) //
|
.withVaultConfigFilename(Constants.VAULTCONFIG_FILENAME) //
|
||||||
.withFilesystemEventConsumer(this::consumeVaultEvent) //
|
.withFilesystemEventConsumer(this::consumeVaultEvent);
|
||||||
.build();
|
if (keyLoader instanceof FilesystemOwnerSupplier oo) {
|
||||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
fsPropsBuilder.withOwnerGetter(oo::getOwner);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CryptoFileSystemProvider.newFileSystem(getPath(), fsPropsBuilder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void destroyCryptoFileSystem() {
|
private void destroyCryptoFileSystem() {
|
||||||
@@ -262,6 +270,7 @@ public class Vault {
|
|||||||
|
|
||||||
private void consumeVaultEvent(FilesystemEvent e) {
|
private void consumeVaultEvent(FilesystemEvent e) {
|
||||||
fileSystemEventAggregator.put(this, e);
|
fileSystemEventAggregator.put(this, e);
|
||||||
|
notificationManager.offer(this, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ******************************************************************************
|
// ******************************************************************************
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.cryptomator.cryptofs.event.BrokenFileNodeEvent;
|
|||||||
import org.cryptomator.cryptofs.event.ConflictResolutionFailedEvent;
|
import org.cryptomator.cryptofs.event.ConflictResolutionFailedEvent;
|
||||||
import org.cryptomator.cryptofs.event.ConflictResolvedEvent;
|
import org.cryptomator.cryptofs.event.ConflictResolvedEvent;
|
||||||
import org.cryptomator.cryptofs.event.DecryptionFailedEvent;
|
import org.cryptomator.cryptofs.event.DecryptionFailedEvent;
|
||||||
|
import org.cryptomator.cryptofs.event.FileIsInUseEvent;
|
||||||
import org.cryptomator.cryptofs.event.FilesystemEvent;
|
import org.cryptomator.cryptofs.event.FilesystemEvent;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -101,6 +102,7 @@ public class FileSystemEventAggregator {
|
|||||||
case ConflictResolutionFailedEvent(_, _, Path conflictingCiphertext, _) -> conflictingCiphertext;
|
case ConflictResolutionFailedEvent(_, _, Path conflictingCiphertext, _) -> conflictingCiphertext;
|
||||||
case BrokenDirFileEvent(_, Path ciphertext) -> ciphertext;
|
case BrokenDirFileEvent(_, Path ciphertext) -> ciphertext;
|
||||||
case BrokenFileNodeEvent(_, _, Path ciphertext) -> ciphertext;
|
case BrokenFileNodeEvent(_, _, Path ciphertext) -> ciphertext;
|
||||||
|
case FileIsInUseEvent(_, _, Path ciphertext, _, _, _) -> ciphertext;
|
||||||
};
|
};
|
||||||
return new FSEventBucket(v, p, event.getClass());
|
return new FSEventBucket(v, p, event.getClass());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package org.cryptomator.event;
|
||||||
|
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.event.FileIsInUseEvent;
|
||||||
|
import org.cryptomator.cryptofs.event.FilesystemEvent;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for notifications.
|
||||||
|
* <p>
|
||||||
|
* To add (filesystem) events, use method {@link #offer(Vault, FilesystemEvent)}. If the input event is eligible, it is added to an internal queue.
|
||||||
|
* An event is eligible, if
|
||||||
|
* <ul>
|
||||||
|
* <li>the event should trigger a notification and</li>
|
||||||
|
* <li>it is not added within the last {@value DEBOUNCE_THRESHOLD_SECONDS} seconds</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @see org.cryptomator.ui.fxapp.FxNotificationManager
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class NotificationManager {
|
||||||
|
|
||||||
|
private static final int DEBOUNCE_THRESHOLD_SECONDS = 5;
|
||||||
|
|
||||||
|
private final Cache<FSEventBucket, FilesystemEvent> debounceCache;
|
||||||
|
private final ConcurrentLinkedQueue<VaultEvent> pendingEvents;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public NotificationManager() {
|
||||||
|
debounceCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(DEBOUNCE_THRESHOLD_SECONDS)).build();
|
||||||
|
pendingEvents = new ConcurrentLinkedQueue<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offers the given filesystem event to the notification manager.
|
||||||
|
*
|
||||||
|
* @param v The vault where the filesystem event happened
|
||||||
|
* @param e the actual filesystem event
|
||||||
|
* @return {@code true} if the filesystem event is accepted, otherwise {@code false}.
|
||||||
|
*/
|
||||||
|
public boolean offer(Vault v, FilesystemEvent e) {
|
||||||
|
return switch (e) {
|
||||||
|
case FileIsInUseEvent fiiue -> addEvent(v, fiiue.ciphertextPath(), fiiue);
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean addEvent(Vault v, Path keyPath, FilesystemEvent e) {
|
||||||
|
var key = new FSEventBucket(v, keyPath, e.getClass());
|
||||||
|
var isAdded = new AtomicBoolean(false);
|
||||||
|
debounceCache.asMap().computeIfAbsent(key, _ -> {
|
||||||
|
synchronized (this) {
|
||||||
|
pendingEvents.add(new VaultEvent(v, e));
|
||||||
|
isAdded.set(true);
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
return isAdded.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all events to the target list and clears afterward the pending-event-queue
|
||||||
|
*
|
||||||
|
* @param target list where the filesystem events are copied to
|
||||||
|
* @return {@code true}, if elements were copied
|
||||||
|
*/
|
||||||
|
public boolean appendToAndClear(List<VaultEvent> target) {
|
||||||
|
//it is not clear, if addAll iterates thread-safe over the pendingEvents
|
||||||
|
//hence we synchronize moving (copy then clear) and adding-single-element operations
|
||||||
|
synchronized (this) {
|
||||||
|
var result = target.addAll(pendingEvents);
|
||||||
|
pendingEvents.clear();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,25 +3,6 @@ package org.cryptomator.event;
|
|||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.cryptofs.event.FilesystemEvent;
|
import org.cryptomator.cryptofs.event.FilesystemEvent;
|
||||||
|
|
||||||
import java.time.Instant;
|
public record VaultEvent(Vault v, FilesystemEvent actualEvent) {
|
||||||
|
|
||||||
public record VaultEvent(Vault v, FilesystemEvent actualEvent, int count) implements Comparable<VaultEvent> {
|
|
||||||
|
|
||||||
public VaultEvent(Vault v, FilesystemEvent actualEvent) {
|
|
||||||
this(v, actualEvent, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(VaultEvent other) {
|
|
||||||
var timeResult = actualEvent.getTimestamp().compareTo(other.actualEvent().getTimestamp());
|
|
||||||
if(timeResult != 0) {
|
|
||||||
return timeResult;
|
|
||||||
} else {
|
|
||||||
return this.equals(other) ? 0 : this.actualEvent.getClass().getName().compareTo(other.actualEvent.getClass().getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public VaultEvent incrementCount(FilesystemEvent update) {
|
|
||||||
return new VaultEvent(v, update, count+1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ abstract class SSLContextDifferentTrustStoreBase implements SSLContextProvider {
|
|||||||
public SSLContext getContext(SecureRandom csprng) throws SSLContextBuildException {
|
public SSLContext getContext(SecureRandom csprng) throws SSLContextBuildException {
|
||||||
try {
|
try {
|
||||||
KeyStore truststore = getTruststore();
|
KeyStore truststore = getTruststore();
|
||||||
ensureLoaded(truststore);
|
truststore.load(null, null);
|
||||||
|
|
||||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||||
tmf.init(truststore);
|
tmf.init(truststore);
|
||||||
@@ -30,13 +30,4 @@ abstract class SSLContextDifferentTrustStoreBase implements SSLContextProvider {
|
|||||||
throw new SSLContextBuildException(e);
|
throw new SSLContextBuildException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ensureLoaded(KeyStore truststore) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
|
|
||||||
try {
|
|
||||||
truststore.aliases();
|
|
||||||
} catch (KeyStoreException e) {
|
|
||||||
// Not initialized yet (e.g. custom KeyStore SPI); initialize without replacing preloaded stores.
|
|
||||||
truststore.load(null, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +1,21 @@
|
|||||||
package org.cryptomator.networking;
|
package org.cryptomator.networking;
|
||||||
|
|
||||||
import org.cryptomator.common.Nullable;
|
|
||||||
import org.cryptomator.integrations.common.OperatingSystem;
|
import org.cryptomator.integrations.common.OperatingSystem;
|
||||||
import org.jetbrains.annotations.VisibleForTesting;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
import java.security.KeyStoreException;
|
import java.security.KeyStoreException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.Provider;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SSLContextProvider for Windows using the Windows certificate store as trust store and the bundled JDK cacerts as fallback
|
* SSLContextProvider for Windows using the Windows certificate store as trust store
|
||||||
* <p>
|
* <p>
|
||||||
* In order to work, the jdk.crypto.mscapi jmod is needed
|
* In order to work, the jdk.crypto.mscapi jmod is needed
|
||||||
*/
|
*/
|
||||||
@OperatingSystem(OperatingSystem.Value.WINDOWS)
|
@OperatingSystem(OperatingSystem.Value.WINDOWS)
|
||||||
public class SSLContextWithWindowsCertStore extends SSLContextDifferentTrustStoreBase implements SSLContextProvider {
|
public class SSLContextWithWindowsCertStore extends SSLContextDifferentTrustStoreBase implements SSLContextProvider {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(SSLContextWithWindowsCertStore.class);
|
|
||||||
private static final String DEFAULT_TRUSTSTORE_PASSWORD = "changeit"; //default JDK cacerts password
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
KeyStore getTruststore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
|
KeyStore getTruststore() throws KeyStoreException {
|
||||||
var windowsKeyStore = KeyStore.getInstance("WINDOWS-ROOT");
|
return KeyStore.getInstance("WINDOWS-ROOT");
|
||||||
var jdkKeyStore = getShippedCaCertsStore();
|
|
||||||
if (jdkKeyStore == null) {
|
|
||||||
return windowsKeyStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureLoaded(windowsKeyStore);
|
|
||||||
ensureLoaded(jdkKeyStore);
|
|
||||||
try {
|
|
||||||
CombinedKeyStoreSpi spi = CombinedKeyStoreSpi.create(windowsKeyStore, jdkKeyStore);
|
|
||||||
Provider dummyProvider = new Provider("CombinedKeyStoreProvider", "1.0", "Provides a combined, read-only KeyStore") {};
|
|
||||||
return new KeyStore(spi, dummyProvider, "CombinedKeyStoreProvider") {};
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
throw new KeyStoreException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
KeyStore getShippedCaCertsStore() {
|
|
||||||
return getCaCertsStoreByProperties(System.getProperties());
|
|
||||||
}
|
|
||||||
|
|
||||||
//for testability
|
|
||||||
@VisibleForTesting
|
|
||||||
@Nullable
|
|
||||||
KeyStore getCaCertsStoreByProperties(Properties props) {
|
|
||||||
var javaHome = Path.of(props.getProperty("java.home"));
|
|
||||||
var trustStorePassword = props.getProperty("javax.net.ssl.trustStorePassword", DEFAULT_TRUSTSTORE_PASSWORD).toCharArray();
|
|
||||||
for (var candidate : List.of(javaHome.resolve("lib/security/cacerts"), javaHome.resolve("conf/security/cacerts"))) {
|
|
||||||
try {
|
|
||||||
if (Files.isRegularFile(candidate)) {
|
|
||||||
return KeyStore.getInstance(candidate.toFile(), trustStorePassword);
|
|
||||||
}
|
|
||||||
} catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException e) {
|
|
||||||
LOG.info("Unable to load fallback cacerts {} file. Skipping fallback.", candidate, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public enum FxmlFile {
|
|||||||
MIGRATION_RUN("/fxml/migration_run.fxml"), //
|
MIGRATION_RUN("/fxml/migration_run.fxml"), //
|
||||||
MIGRATION_START("/fxml/migration_start.fxml"), //
|
MIGRATION_START("/fxml/migration_start.fxml"), //
|
||||||
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
|
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
|
||||||
|
NOTIFICATION("/fxml/notification.fxml"), //
|
||||||
PREFERENCES("/fxml/preferences.fxml"), //
|
PREFERENCES("/fxml/preferences.fxml"), //
|
||||||
QUIT("/fxml/quit.fxml"), //
|
QUIT("/fxml/quit.fxml"), //
|
||||||
QUIT_FORCED("/fxml/quit_forced.fxml"), //
|
QUIT_FORCED("/fxml/quit_forced.fxml"), //
|
||||||
@@ -59,6 +60,7 @@ public enum FxmlFile {
|
|||||||
VAULT_STATISTICS("/fxml/stats.fxml"), //
|
VAULT_STATISTICS("/fxml/stats.fxml"), //
|
||||||
WRONGFILEALERT("/fxml/wrongfilealert.fxml");
|
WRONGFILEALERT("/fxml/wrongfilealert.fxml");
|
||||||
|
|
||||||
|
|
||||||
private final String ressourcePathString;
|
private final String ressourcePathString;
|
||||||
|
|
||||||
FxmlFile(String ressourcePathString) {
|
FxmlFile(String ressourcePathString) {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
|
import javafx.stage.Screen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class providing methods regarding the OS bar.
|
||||||
|
*/
|
||||||
|
public class SystemBarUtil {
|
||||||
|
|
||||||
|
public enum Placement {
|
||||||
|
/**
|
||||||
|
* OS Bar placed at the left screen edge
|
||||||
|
*/
|
||||||
|
LEFT,
|
||||||
|
/**
|
||||||
|
* OS Bar placed at the top screen edge
|
||||||
|
*/
|
||||||
|
TOP,
|
||||||
|
/**
|
||||||
|
* OS Bar placed at the right screen edge
|
||||||
|
*/
|
||||||
|
RIGHT,
|
||||||
|
/**
|
||||||
|
* OS Bar placed at the bottom screen edge
|
||||||
|
*/
|
||||||
|
BOTTOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the placement of the OS bar on the given screen.
|
||||||
|
* <p>
|
||||||
|
* <b>Assuming the OS bar fills one screen edge completely</b>,
|
||||||
|
* this method determines that screen edge by comparing the actual screen bounds with the visual ones.
|
||||||
|
* <p>
|
||||||
|
* If the screen does not have a system bar, the bottom placement is returned.
|
||||||
|
* If the screen does have multiple system bars, the first in following priority is returned:
|
||||||
|
* LEFT, TOP, RIGHT, BOTTOM.
|
||||||
|
*
|
||||||
|
* @param screen a {@link Screen} where an OS bar exists
|
||||||
|
* @return {@link Placement} indicating the screen edge.
|
||||||
|
*/
|
||||||
|
public static Placement getPlacementOfSystembar(Screen screen) {
|
||||||
|
var bounds = screen.getBounds();
|
||||||
|
var vBounds = screen.getVisualBounds();
|
||||||
|
//assumption: the system bar fills a whole screen side
|
||||||
|
if (bounds.getMinX() != vBounds.getMinX()) {
|
||||||
|
return Placement.LEFT;
|
||||||
|
} else if (bounds.getMinY() != vBounds.getMinY()) {
|
||||||
|
return Placement.TOP;
|
||||||
|
} else if (bounds.getMaxX() != vBounds.getMaxX()) {
|
||||||
|
return Placement.RIGHT;
|
||||||
|
} else {
|
||||||
|
return Placement.BOTTOM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ public enum FontAwesome5Icon {
|
|||||||
CARET_DOWN("\uF0D7"), //
|
CARET_DOWN("\uF0D7"), //
|
||||||
CARET_RIGHT("\uF0Da"), //
|
CARET_RIGHT("\uF0Da"), //
|
||||||
CHECK("\uF00C"), //
|
CHECK("\uF00C"), //
|
||||||
|
CHEVRON_LEFT("\uF053"), //
|
||||||
|
CHEVRON_RIGHT("\uF054"), //
|
||||||
CLOCK("\uF017"), //
|
CLOCK("\uF017"), //
|
||||||
CLIPBOARD("\uF328"), //
|
CLIPBOARD("\uF328"), //
|
||||||
COG("\uF013"), //
|
COG("\uF013"), //
|
||||||
@@ -60,6 +62,7 @@ public enum FontAwesome5Icon {
|
|||||||
TRASH("\uF1F8"), //
|
TRASH("\uF1F8"), //
|
||||||
UNLINK("\uf127"), //
|
UNLINK("\uf127"), //
|
||||||
USER_COG("\uf4fe"), //
|
USER_COG("\uf4fe"), //
|
||||||
|
USER_LOCK("\uf502"), //
|
||||||
WRENCH("\uF0AD"), //
|
WRENCH("\uF0AD"), //
|
||||||
WINDOW_MINIMIZE("\uF2D1"), //
|
WINDOW_MINIMIZE("\uF2D1"), //
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package org.cryptomator.ui.eventview;
|
package org.cryptomator.ui.eventview;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
|
import org.cryptomator.common.Constants;
|
||||||
|
import org.cryptomator.cryptofs.event.FileIsInUseEvent;
|
||||||
import org.cryptomator.event.FSEventBucket;
|
import org.cryptomator.event.FSEventBucket;
|
||||||
import org.cryptomator.event.FSEventBucketContent;
|
import org.cryptomator.event.FSEventBucketContent;
|
||||||
import org.cryptomator.event.FileSystemEventAggregator;
|
import org.cryptomator.event.FileSystemEventAggregator;
|
||||||
@@ -115,7 +118,7 @@ public class EventListCellController implements FxController {
|
|||||||
eventActionsMenu.hide();
|
eventActionsMenu.hide();
|
||||||
eventActionsMenu.getItems().clear();
|
eventActionsMenu.getItems().clear();
|
||||||
eventTooltip.setText(item.getKey().vault().getDisplayName());
|
eventTooltip.setText(item.getKey().vault().getDisplayName());
|
||||||
addAction("generic.action.dismiss", () -> {
|
addLocalizedAction("generic.action.dismiss", () -> {
|
||||||
fileSystemEventAggregator.remove(item.getKey());
|
fileSystemEventAggregator.remove(item.getKey());
|
||||||
});
|
});
|
||||||
switch (item.getValue().mostRecentEvent()) {
|
switch (item.getValue().mostRecentEvent()) {
|
||||||
@@ -124,20 +127,41 @@ public class EventListCellController implements FxController {
|
|||||||
case DecryptionFailedEvent fse -> this.adjustToDecryptionFailedEvent(fse);
|
case DecryptionFailedEvent fse -> this.adjustToDecryptionFailedEvent(fse);
|
||||||
case BrokenDirFileEvent fse -> this.adjustToBrokenDirFileEvent(fse);
|
case BrokenDirFileEvent fse -> this.adjustToBrokenDirFileEvent(fse);
|
||||||
case BrokenFileNodeEvent fse -> this.adjustToBrokenFileNodeEvent(fse);
|
case BrokenFileNodeEvent fse -> this.adjustToBrokenFileNodeEvent(fse);
|
||||||
|
case FileIsInUseEvent fse -> this.adjustToFileInUseEvent(fse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void adjustToFileInUseEvent(FileIsInUseEvent fiiue) {
|
||||||
|
eventIcon.setValue(FontAwesome5Icon.USER_LOCK);
|
||||||
|
eventMessage.setValue(resourceBundle.getString("eventView.entry.inUse.message"));
|
||||||
|
var indexFileName = fiiue.cleartextPath().lastIndexOf("/");
|
||||||
|
eventDescription.setValue(fiiue.cleartextPath().substring(indexFileName + 1));
|
||||||
|
if (revealService != null) {
|
||||||
|
addLocalizedAction("eventView.entry.inUse.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(fiiue.cleartextPath())));
|
||||||
|
addLocalizedAction("eventView.entry.inUse.showEncrypted", () -> reveal(revealService, fiiue.ciphertextPath()));
|
||||||
|
} else {
|
||||||
|
addLocalizedAction("eventView.entry.inUse.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(fiiue.cleartextPath()).toString()));
|
||||||
|
addLocalizedAction("eventView.entry.inUse.copyEncrypted", () -> copyToClipboard(fiiue.ciphertextPath().toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
var userAndDevice = fiiue.owner().split(Constants.HUB_USER_DEVICE_SEPARATOR);
|
||||||
|
var user = userAndDevice[0];
|
||||||
|
var device = userAndDevice.length == 1 ? userAndDevice[0] : userAndDevice[1];
|
||||||
|
addLocalizedAction("eventView.entry.inUse.copyUserAndDevice", () -> copyToClipboard(user + ", " + device));
|
||||||
|
addLocalizedAction("eventView.entry.inUse.ignoreLock", fiiue.ignoreMethod());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void adjustToBrokenFileNodeEvent(BrokenFileNodeEvent bfe) {
|
private void adjustToBrokenFileNodeEvent(BrokenFileNodeEvent bfe) {
|
||||||
eventIcon.setValue(FontAwesome5Icon.TIMES);
|
eventIcon.setValue(FontAwesome5Icon.TIMES);
|
||||||
eventMessage.setValue(resourceBundle.getString("eventView.entry.brokenFileNode.message"));
|
eventMessage.setValue(resourceBundle.getString("eventView.entry.brokenFileNode.message"));
|
||||||
eventDescription.setValue(bfe.ciphertextPath().getFileName().toString());
|
eventDescription.setValue(bfe.ciphertextPath().getFileName().toString());
|
||||||
if (revealService != null) {
|
if (revealService != null) {
|
||||||
addAction("eventView.entry.brokenFileNode.showEncrypted", () -> reveal(revealService, bfe.ciphertextPath()));
|
addLocalizedAction("eventView.entry.brokenFileNode.showEncrypted", () -> reveal(revealService, bfe.ciphertextPath()));
|
||||||
} else {
|
} else {
|
||||||
addAction("eventView.entry.brokenFileNode.copyEncrypted", () -> copyToClipboard(bfe.ciphertextPath().toString()));
|
addLocalizedAction("eventView.entry.brokenFileNode.copyEncrypted", () -> copyToClipboard(bfe.ciphertextPath().toString()));
|
||||||
}
|
}
|
||||||
addAction("eventView.entry.brokenFileNode.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(bfe.cleartextPath()).toString()));
|
addLocalizedAction("eventView.entry.brokenFileNode.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(bfe.cleartextPath()).toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void adjustToConflictResolvedEvent(ConflictResolvedEvent cre) {
|
private void adjustToConflictResolvedEvent(ConflictResolvedEvent cre) {
|
||||||
@@ -145,9 +169,9 @@ public class EventListCellController implements FxController {
|
|||||||
eventMessage.setValue(resourceBundle.getString("eventView.entry.conflictResolved.message"));
|
eventMessage.setValue(resourceBundle.getString("eventView.entry.conflictResolved.message"));
|
||||||
eventDescription.setValue(cre.resolvedCiphertextPath().getFileName().toString());
|
eventDescription.setValue(cre.resolvedCiphertextPath().getFileName().toString());
|
||||||
if (revealService != null) {
|
if (revealService != null) {
|
||||||
addAction("eventView.entry.conflictResolved.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cre.resolvedCleartextPath())));
|
addLocalizedAction("eventView.entry.conflictResolved.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cre.resolvedCleartextPath())));
|
||||||
} else {
|
} else {
|
||||||
addAction("eventView.entry.conflictResolved.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cre.resolvedCleartextPath()).toString()));
|
addLocalizedAction("eventView.entry.conflictResolved.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cre.resolvedCleartextPath()).toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,11 +180,11 @@ public class EventListCellController implements FxController {
|
|||||||
eventMessage.setValue(resourceBundle.getString("eventView.entry.conflict.message"));
|
eventMessage.setValue(resourceBundle.getString("eventView.entry.conflict.message"));
|
||||||
eventDescription.setValue(cfe.conflictingCiphertextPath().getFileName().toString());
|
eventDescription.setValue(cfe.conflictingCiphertextPath().getFileName().toString());
|
||||||
if (revealService != null) {
|
if (revealService != null) {
|
||||||
addAction("eventView.entry.conflict.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cfe.canonicalCleartextPath())));
|
addLocalizedAction("eventView.entry.conflict.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cfe.canonicalCleartextPath())));
|
||||||
addAction("eventView.entry.conflict.showEncrypted", () -> reveal(revealService, cfe.conflictingCiphertextPath()));
|
addLocalizedAction("eventView.entry.conflict.showEncrypted", () -> reveal(revealService, cfe.conflictingCiphertextPath()));
|
||||||
} else {
|
} else {
|
||||||
addAction("eventView.entry.conflict.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cfe.canonicalCleartextPath()).toString()));
|
addLocalizedAction("eventView.entry.conflict.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cfe.canonicalCleartextPath()).toString()));
|
||||||
addAction("eventView.entry.conflict.copyEncrypted", () -> copyToClipboard(cfe.conflictingCiphertextPath().toString()));
|
addLocalizedAction("eventView.entry.conflict.copyEncrypted", () -> copyToClipboard(cfe.conflictingCiphertextPath().toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,9 +193,9 @@ public class EventListCellController implements FxController {
|
|||||||
eventMessage.setValue(resourceBundle.getString("eventView.entry.decryptionFailed.message"));
|
eventMessage.setValue(resourceBundle.getString("eventView.entry.decryptionFailed.message"));
|
||||||
eventDescription.setValue(dfe.ciphertextPath().getFileName().toString());
|
eventDescription.setValue(dfe.ciphertextPath().getFileName().toString());
|
||||||
if (revealService != null) {
|
if (revealService != null) {
|
||||||
addAction("eventView.entry.decryptionFailed.showEncrypted", () -> reveal(revealService, dfe.ciphertextPath()));
|
addLocalizedAction("eventView.entry.decryptionFailed.showEncrypted", () -> reveal(revealService, dfe.ciphertextPath()));
|
||||||
} else {
|
} else {
|
||||||
addAction("eventView.entry.decryptionFailed.copyEncrypted", () -> copyToClipboard(dfe.ciphertextPath().toString()));
|
addLocalizedAction("eventView.entry.decryptionFailed.copyEncrypted", () -> copyToClipboard(dfe.ciphertextPath().toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,14 +204,19 @@ public class EventListCellController implements FxController {
|
|||||||
eventMessage.setValue(resourceBundle.getString("eventView.entry.brokenDirFile.message"));
|
eventMessage.setValue(resourceBundle.getString("eventView.entry.brokenDirFile.message"));
|
||||||
eventDescription.setValue(bde.ciphertextPath().getParent().getFileName().toString());
|
eventDescription.setValue(bde.ciphertextPath().getParent().getFileName().toString());
|
||||||
if (revealService != null) {
|
if (revealService != null) {
|
||||||
addAction("eventView.entry.brokenDirFile.showEncrypted", () -> reveal(revealService, bde.ciphertextPath()));
|
addLocalizedAction("eventView.entry.brokenDirFile.showEncrypted", () -> reveal(revealService, bde.ciphertextPath()));
|
||||||
} else {
|
} else {
|
||||||
addAction("eventView.entry.brokenDirFile.copyEncrypted", () -> copyToClipboard(bde.ciphertextPath().toString()));
|
addLocalizedAction("eventView.entry.brokenDirFile.copyEncrypted", () -> copyToClipboard(bde.ciphertextPath().toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addAction(String localizationKey, Runnable action) {
|
private void addLocalizedAction(String localizationKey, Runnable action) {
|
||||||
var entry = new MenuItem(resourceBundle.getString(localizationKey));
|
var entryText = resourceBundle.getString(localizationKey);
|
||||||
|
addAction(entryText, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAction(String entryText, Runnable action) {
|
||||||
|
var entry = new MenuItem(entryText);
|
||||||
entry.getStyleClass().addLast("dropdown-button-context-menu-item");
|
entry.getStyleClass().addLast("dropdown-button-context-menu-item");
|
||||||
entry.setOnAction(_ -> action.run());
|
entry.setOnAction(_ -> action.run());
|
||||||
eventActionsMenu.getItems().addLast(entry);
|
eventActionsMenu.getItems().addLast(entry);
|
||||||
@@ -234,18 +263,17 @@ public class EventListCellController implements FxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path convertVaultPathToSystemPath(Path p) {
|
private Path convertVaultPathToSystemPath(String vaultInternalPath) {
|
||||||
if (!(p instanceof CryptoPath)) {
|
|
||||||
throw new IllegalArgumentException("Path " + p + " is not a vault path");
|
|
||||||
}
|
|
||||||
var v = eventEntry.getValue().getKey().vault();
|
var v = eventEntry.getValue().getKey().vault();
|
||||||
if (!v.isUnlocked()) {
|
if (!v.isUnlocked()) {
|
||||||
return Path.of(System.getProperty("user.home"));
|
return Path.of(System.getProperty("user.home"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var mountUri = v.getMountPoint().uri();
|
var mountPoint = v.getMountPoint().uri().getPath();
|
||||||
var internalPath = p.toString().substring(1);
|
if(SystemUtils.IS_OS_WINDOWS) {
|
||||||
return Path.of(mountUri.getPath().concat(internalPath).substring(1));
|
mountPoint = mountPoint.substring(1); //strip away any leading "/", otherwise there are errors
|
||||||
|
}
|
||||||
|
return Path.of(mountPoint, vaultInternalPath.substring(1)); //vaultPaths are always absolute
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reveal(RevealPathService s, Path p) {
|
private void reveal(RevealPathService s, Path p) {
|
||||||
|
|||||||
@@ -30,9 +30,20 @@ public class FxApplication {
|
|||||||
private final FxApplicationTerminator applicationTerminator;
|
private final FxApplicationTerminator applicationTerminator;
|
||||||
private final AutoUnlocker autoUnlocker;
|
private final AutoUnlocker autoUnlocker;
|
||||||
private final FxFSEventList fxFSEventList;
|
private final FxFSEventList fxFSEventList;
|
||||||
|
private final FxNotificationManager notificationManager;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
FxApplication(@Named("startupTime") long startupTime, Environment environment, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy<TrayMenuComponent> trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker, FxFSEventList fxFSEventList) {
|
FxApplication(@Named("startupTime") long startupTime, //
|
||||||
|
Environment environment, //
|
||||||
|
Settings settings, //
|
||||||
|
AppLaunchEventHandler launchEventHandler, //
|
||||||
|
Lazy<TrayMenuComponent> trayMenu, //
|
||||||
|
FxApplicationWindows appWindows, //
|
||||||
|
FxApplicationStyle applicationStyle, //
|
||||||
|
FxApplicationTerminator applicationTerminator, //
|
||||||
|
AutoUnlocker autoUnlocker, //
|
||||||
|
FxFSEventList fxFSEventList, //
|
||||||
|
FxNotificationManager notificationManager) {
|
||||||
this.startupTime = startupTime;
|
this.startupTime = startupTime;
|
||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
@@ -43,6 +54,7 @@ public class FxApplication {
|
|||||||
this.applicationTerminator = applicationTerminator;
|
this.applicationTerminator = applicationTerminator;
|
||||||
this.autoUnlocker = autoUnlocker;
|
this.autoUnlocker = autoUnlocker;
|
||||||
this.fxFSEventList = fxFSEventList;
|
this.fxFSEventList = fxFSEventList;
|
||||||
|
this.notificationManager = notificationManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
@@ -88,6 +100,7 @@ public class FxApplication {
|
|||||||
|
|
||||||
launchEventHandler.startHandlingLaunchEvents();
|
launchEventHandler.startHandlingLaunchEvents();
|
||||||
fxFSEventList.schedulePollForUpdates();
|
fxFSEventList.schedulePollForUpdates();
|
||||||
|
notificationManager.schedulePollForUpdates();
|
||||||
autoUnlocker.tryUnlockForTimespan(2, TimeUnit.MINUTES);
|
autoUnlocker.tryUnlockForTimespan(2, TimeUnit.MINUTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.cryptomator.ui.eventview.EventViewComponent;
|
|||||||
import org.cryptomator.ui.health.HealthCheckComponent;
|
import org.cryptomator.ui.health.HealthCheckComponent;
|
||||||
import org.cryptomator.ui.lock.LockComponent;
|
import org.cryptomator.ui.lock.LockComponent;
|
||||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||||
|
import org.cryptomator.ui.notification.NotificationComponent;
|
||||||
import org.cryptomator.ui.preferences.PreferencesComponent;
|
import org.cryptomator.ui.preferences.PreferencesComponent;
|
||||||
import org.cryptomator.ui.quit.QuitComponent;
|
import org.cryptomator.ui.quit.QuitComponent;
|
||||||
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
|
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
|
||||||
@@ -26,7 +27,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, //
|
||||||
@@ -39,7 +40,8 @@ import java.io.InputStream;
|
|||||||
UpdateReminderComponent.class, //
|
UpdateReminderComponent.class, //
|
||||||
ShareVaultComponent.class, //
|
ShareVaultComponent.class, //
|
||||||
EventViewComponent.class, //
|
EventViewComponent.class, //
|
||||||
RecoveryKeyComponent.class})
|
RecoveryKeyComponent.class, //
|
||||||
|
NotificationComponent.class })
|
||||||
abstract class FxApplicationModule {
|
abstract class FxApplicationModule {
|
||||||
|
|
||||||
private static Image createImageFromResource(String resourceName) throws IOException {
|
private static Image createImageFromResource(String resourceName) throws IOException {
|
||||||
@@ -78,4 +80,10 @@ abstract class FxApplicationModule {
|
|||||||
return factory.create();
|
return factory.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxApplicationScoped
|
||||||
|
static NotificationComponent provideNotificationComponent(NotificationComponent.Factory factory) {
|
||||||
|
return factory.create();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import org.cryptomator.ui.error.ErrorComponent;
|
|||||||
import org.cryptomator.ui.eventview.EventViewComponent;
|
import org.cryptomator.ui.eventview.EventViewComponent;
|
||||||
import org.cryptomator.ui.lock.LockComponent;
|
import org.cryptomator.ui.lock.LockComponent;
|
||||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||||
|
import org.cryptomator.ui.notification.NotificationComponent;
|
||||||
import org.cryptomator.ui.preferences.PreferencesComponent;
|
import org.cryptomator.ui.preferences.PreferencesComponent;
|
||||||
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
||||||
import org.cryptomator.ui.quit.QuitComponent;
|
import org.cryptomator.ui.quit.QuitComponent;
|
||||||
@@ -54,6 +55,7 @@ public class FxApplicationWindows {
|
|||||||
private final LockComponent.Factory lockWorkflowFactory;
|
private final LockComponent.Factory lockWorkflowFactory;
|
||||||
private final ErrorComponent.Factory errorWindowFactory;
|
private final ErrorComponent.Factory errorWindowFactory;
|
||||||
private final Lazy<EventViewComponent> eventViewWindow;
|
private final Lazy<EventViewComponent> eventViewWindow;
|
||||||
|
private final Lazy<NotificationComponent> notificationWindow;
|
||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
private final VaultOptionsComponent.Factory vaultOptionsWindow;
|
private final VaultOptionsComponent.Factory vaultOptionsWindow;
|
||||||
private final ShareVaultComponent.Factory shareVaultWindow;
|
private final ShareVaultComponent.Factory shareVaultWindow;
|
||||||
@@ -73,6 +75,7 @@ public class FxApplicationWindows {
|
|||||||
VaultOptionsComponent.Factory vaultOptionsWindow, //
|
VaultOptionsComponent.Factory vaultOptionsWindow, //
|
||||||
ShareVaultComponent.Factory shareVaultWindow, //
|
ShareVaultComponent.Factory shareVaultWindow, //
|
||||||
Lazy<EventViewComponent> eventViewWindow, //
|
Lazy<EventViewComponent> eventViewWindow, //
|
||||||
|
Lazy<NotificationComponent> notificationWindow,
|
||||||
ExecutorService executor, //
|
ExecutorService executor, //
|
||||||
Dialogs dialogs) {
|
Dialogs dialogs) {
|
||||||
this.primaryStage = primaryStage;
|
this.primaryStage = primaryStage;
|
||||||
@@ -85,6 +88,7 @@ public class FxApplicationWindows {
|
|||||||
this.lockWorkflowFactory = lockWorkflowFactory;
|
this.lockWorkflowFactory = lockWorkflowFactory;
|
||||||
this.errorWindowFactory = errorWindowFactory;
|
this.errorWindowFactory = errorWindowFactory;
|
||||||
this.eventViewWindow = eventViewWindow;
|
this.eventViewWindow = eventViewWindow;
|
||||||
|
this.notificationWindow = notificationWindow;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.vaultOptionsWindow = vaultOptionsWindow;
|
this.vaultOptionsWindow = vaultOptionsWindow;
|
||||||
this.shareVaultWindow = shareVaultWindow;
|
this.shareVaultWindow = shareVaultWindow;
|
||||||
@@ -193,6 +197,10 @@ public class FxApplicationWindows {
|
|||||||
return CompletableFuture.supplyAsync(() -> eventViewWindow.get().showEventViewerWindow(), Platform::runLater).whenComplete(this::reportErrors);
|
return CompletableFuture.supplyAsync(() -> eventViewWindow.get().showEventViewerWindow(), Platform::runLater).whenComplete(this::reportErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CompletionStage<Stage> showNotification() {
|
||||||
|
return CompletableFuture.supplyAsync(() -> notificationWindow.get().showNotification(), Platform::runLater).whenComplete(this::reportErrors);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the generic error scene in the given window.
|
* Displays the generic error scene in the given window.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.cryptomator.ui.fxapp;
|
||||||
|
|
||||||
|
import org.cryptomator.event.NotificationManager;
|
||||||
|
import org.cryptomator.event.VaultEvent;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification manager inside the UI domain.
|
||||||
|
* <p>
|
||||||
|
* Polls the {@link NotificationManager} for pending events every {@value POLL_INTERVAL_SECONDS } seconds and
|
||||||
|
* triggers the notification window display when events are available.
|
||||||
|
* Returns an observable list of events requiring a user notification with {@link #getEventsRequiringNotification()}.
|
||||||
|
*
|
||||||
|
* @see NotificationManager
|
||||||
|
*/
|
||||||
|
@FxApplicationScoped
|
||||||
|
public class FxNotificationManager {
|
||||||
|
|
||||||
|
private static final int POLL_INTERVAL_SECONDS = 1;
|
||||||
|
|
||||||
|
private final NotificationManager notificationManager;
|
||||||
|
private final ScheduledExecutorService scheduler;
|
||||||
|
private final FxApplicationWindows applicationWindows;
|
||||||
|
private final ObservableList<VaultEvent> eventsRequiringNotification;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public FxNotificationManager(NotificationManager notificationManager, ScheduledExecutorService scheduler, FxApplicationWindows applicationWindows) {
|
||||||
|
this.notificationManager = notificationManager;
|
||||||
|
this.scheduler = scheduler;
|
||||||
|
this.applicationWindows = applicationWindows;
|
||||||
|
this.eventsRequiringNotification = FXCollections.observableArrayList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void schedulePollForUpdates() {
|
||||||
|
scheduler.scheduleAtFixedRate(this::checkForPendingNotifications, 0, POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkForPendingNotifications() {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (notificationManager.appendToAndClear(eventsRequiringNotification)) {
|
||||||
|
applicationWindows.showNotification();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableList<VaultEvent> getEventsRequiringNotification() {
|
||||||
|
return eventsRequiringNotification;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -63,8 +63,8 @@ abstract class HealthCheckModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@HealthCheckWindow
|
@HealthCheckWindow
|
||||||
@HealthCheckScoped
|
@HealthCheckScoped
|
||||||
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) {
|
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Factory compFactory, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) {
|
||||||
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
|
return compFactory.create(vault, window).keyloadingStrategy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ package org.cryptomator.ui.keyloading;
|
|||||||
import dagger.BindsInstance;
|
import dagger.BindsInstance;
|
||||||
import dagger.Subcomponent;
|
import dagger.Subcomponent;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
|
||||||
|
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
@KeyLoadingScoped
|
@KeyLoadingScoped
|
||||||
@Subcomponent(modules = {KeyLoadingModule.class})
|
@Subcomponent(modules = {KeyLoadingModule.class})
|
||||||
@@ -16,16 +13,10 @@ public interface KeyLoadingComponent {
|
|||||||
@KeyLoading
|
@KeyLoading
|
||||||
KeyLoadingStrategy keyloadingStrategy();
|
KeyLoadingStrategy keyloadingStrategy();
|
||||||
|
|
||||||
@Subcomponent.Builder
|
@Subcomponent.Factory
|
||||||
interface Builder {
|
interface Factory {
|
||||||
|
|
||||||
@BindsInstance
|
KeyLoadingComponent create(@BindsInstance @KeyLoading Vault vault, @KeyLoading @BindsInstance Stage window);
|
||||||
Builder vault(@KeyLoading Vault vault);
|
|
||||||
|
|
||||||
@BindsInstance
|
|
||||||
Builder window(@KeyLoading Stage window);
|
|
||||||
|
|
||||||
KeyLoadingComponent build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ public abstract class HubKeyLoadingModule {
|
|||||||
return new AtomicReference<>();
|
return new AtomicReference<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("filesystemOwnerId")
|
||||||
|
@KeyLoadingScoped
|
||||||
|
static AtomicReference<String> provideFilesystemOwnerIdRef() {
|
||||||
|
return new AtomicReference<>();
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@KeyLoadingScoped
|
@KeyLoadingScoped
|
||||||
static CompletableFuture<ReceivedKey> provideResult() {
|
static CompletableFuture<ReceivedKey> provideResult() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.cryptomator.ui.keyloading.hub;
|
|||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import dagger.Lazy;
|
import dagger.Lazy;
|
||||||
|
import org.cryptomator.common.FilesystemOwnerSupplier;
|
||||||
import org.cryptomator.common.keychain.KeychainManager;
|
import org.cryptomator.common.keychain.KeychainManager;
|
||||||
import org.cryptomator.common.keychain.NoKeychainAccessProviderException;
|
import org.cryptomator.common.keychain.NoKeychainAccessProviderException;
|
||||||
import org.cryptomator.common.settings.DeviceKey;
|
import org.cryptomator.common.settings.DeviceKey;
|
||||||
@@ -23,25 +24,28 @@ import java.net.URI;
|
|||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
@KeyLoading
|
@KeyLoading
|
||||||
public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
public class HubKeyLoadingStrategy implements KeyLoadingStrategy, FilesystemOwnerSupplier {
|
||||||
|
|
||||||
private static final String SCHEME_PREFIX = "hub+";
|
public static final String SCHEME_PREFIX = "hub+";
|
||||||
public static final String SCHEME_HUB_HTTP = SCHEME_PREFIX + "http";
|
public static final String SCHEME_HUB_HTTP = SCHEME_PREFIX + "http";
|
||||||
public static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https";
|
public static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https";
|
||||||
|
|
||||||
private final Stage window;
|
private final Stage window;
|
||||||
private final KeychainManager keychainManager;
|
private final KeychainManager keychainManager;
|
||||||
|
private final AtomicReference<String> fsOwnerId;
|
||||||
private final Lazy<Scene> authFlowScene;
|
private final Lazy<Scene> authFlowScene;
|
||||||
private final Lazy<Scene> noKeychainScene;
|
private final Lazy<Scene> noKeychainScene;
|
||||||
private final CompletableFuture<ReceivedKey> result;
|
private final CompletableFuture<ReceivedKey> result;
|
||||||
private final DeviceKey deviceKey;
|
private final DeviceKey deviceKey;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy<Scene> noKeychainScene, CompletableFuture<ReceivedKey> result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) {
|
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy<Scene> noKeychainScene, CompletableFuture<ReceivedKey> result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle, @Named("filesystemOwnerId") AtomicReference<String> fsOwnerId) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.keychainManager = keychainManager;
|
this.keychainManager = keychainManager;
|
||||||
|
this.fsOwnerId = fsOwnerId;
|
||||||
window.setTitle(windowTitle);
|
window.setTitle(windowTitle);
|
||||||
window.setOnCloseRequest(_ -> result.cancel(true));
|
window.setOnCloseRequest(_ -> result.cancel(true));
|
||||||
this.authFlowScene = authFlowScene;
|
this.authFlowScene = authFlowScene;
|
||||||
@@ -90,4 +94,13 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOwner() {
|
||||||
|
var name = fsOwnerId.get();
|
||||||
|
if (name == null) {
|
||||||
|
throw new IllegalStateException("Owner is not yet determined");
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.nimbusds.jose.JWEObject;
|
import com.nimbusds.jose.JWEObject;
|
||||||
import dagger.Lazy;
|
import dagger.Lazy;
|
||||||
|
import org.cryptomator.common.Constants;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.common.FxmlFile;
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
@@ -41,7 +42,6 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
public class ReceiveKeyController implements FxController {
|
public class ReceiveKeyController implements FxController {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ReceiveKeyController.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ReceiveKeyController.class);
|
||||||
private static final String SCHEME_PREFIX = "hub+";
|
|
||||||
private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true);
|
private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true);
|
||||||
private static final Duration REQ_TIMEOUT = Duration.ofSeconds(10);
|
private static final Duration REQ_TIMEOUT = Duration.ofSeconds(10);
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ public class ReceiveKeyController implements FxController {
|
|||||||
private final String vaultId;
|
private final String vaultId;
|
||||||
private final String deviceId;
|
private final String deviceId;
|
||||||
private final String bearerToken;
|
private final String bearerToken;
|
||||||
|
private final AtomicReference<String> fsOwnerId;
|
||||||
private final CompletableFuture<ReceivedKey> result;
|
private final CompletableFuture<ReceivedKey> result;
|
||||||
private final Lazy<Scene> registerDeviceScene;
|
private final Lazy<Scene> registerDeviceScene;
|
||||||
private final Lazy<Scene> legacyRegisterDeviceScene;
|
private final Lazy<Scene> legacyRegisterDeviceScene;
|
||||||
@@ -59,12 +60,25 @@ public class ReceiveKeyController implements FxController {
|
|||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, HubConfig hubConfig, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<ReceivedKey> result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy<Scene> legacyRegisterDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy<Scene> unauthorizedScene, @FxmlScene(FxmlFile.HUB_REQUIRE_ACCOUNT_INIT) Lazy<Scene> accountInitializationScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy<Scene> invalidLicenseScene) {
|
public ReceiveKeyController(@KeyLoading Vault vault, //
|
||||||
|
ExecutorService executor, //
|
||||||
|
@KeyLoading Stage window, //
|
||||||
|
HubConfig hubConfig, //
|
||||||
|
@Named("deviceId") String deviceId, //
|
||||||
|
@Named("bearerToken") AtomicReference<String> tokenRef, //
|
||||||
|
@Named("filesystemOwnerId") AtomicReference<String> fsOwnerId, //
|
||||||
|
CompletableFuture<ReceivedKey> result, //
|
||||||
|
@FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, //
|
||||||
|
@FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy<Scene> legacyRegisterDeviceScene, //
|
||||||
|
@FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy<Scene> unauthorizedScene, //
|
||||||
|
@FxmlScene(FxmlFile.HUB_REQUIRE_ACCOUNT_INIT) Lazy<Scene> accountInitializationScene, //
|
||||||
|
@FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy<Scene> invalidLicenseScene) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.hubConfig = hubConfig;
|
this.hubConfig = hubConfig;
|
||||||
this.vaultId = extractVaultId(vault.getVaultConfigCache().getUnchecked().getKeyId()); // TODO: access vault config's JTI directly (requires changes in cryptofs)
|
this.vaultId = extractVaultId(vault.getVaultConfigCache().getUnchecked().getKeyId()); // TODO: access vault config's JTI directly (requires changes in cryptofs)
|
||||||
this.deviceId = deviceId;
|
this.deviceId = deviceId;
|
||||||
this.bearerToken = Objects.requireNonNull(tokenRef.get());
|
this.bearerToken = Objects.requireNonNull(tokenRef.get());
|
||||||
|
this.fsOwnerId = fsOwnerId;
|
||||||
this.result = result;
|
this.result = result;
|
||||||
this.registerDeviceScene = registerDeviceScene;
|
this.registerDeviceScene = registerDeviceScene;
|
||||||
this.legacyRegisterDeviceScene = legacyRegisterDeviceScene;
|
this.legacyRegisterDeviceScene = legacyRegisterDeviceScene;
|
||||||
@@ -81,7 +95,34 @@ public class ReceiveKeyController implements FxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void receiveKey() {
|
public void receiveKey() {
|
||||||
requestApiConfig();
|
requestUserData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestUserData() {
|
||||||
|
var userUri = hubConfig.URIs.API.resolve("users/me?withDevices=false");
|
||||||
|
var request = HttpRequest.newBuilder(userUri) //
|
||||||
|
.header("Authorization", "Bearer " + bearerToken) //
|
||||||
|
.GET() //
|
||||||
|
.timeout(REQ_TIMEOUT) //
|
||||||
|
.build();
|
||||||
|
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) //
|
||||||
|
.thenAcceptAsync(this::receivedUserData) //
|
||||||
|
.exceptionally(this::retrievalFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void receivedUserData(HttpResponse<String> response) {
|
||||||
|
LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
|
||||||
|
try {
|
||||||
|
if (response.statusCode() == 200) {
|
||||||
|
var user = JSON.reader().readValue(response.body(), UserDto.class);
|
||||||
|
fsOwnerId.set(user.name);
|
||||||
|
requestApiConfig();
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unexpected response " + response.statusCode());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,6 +185,7 @@ public class ReceiveKeyController implements FxController {
|
|||||||
switch (response.statusCode()) {
|
switch (response.statusCode()) {
|
||||||
case 200 -> {
|
case 200 -> {
|
||||||
var device = JSON.reader().readValue(response.body(), DeviceDto.class);
|
var device = JSON.reader().readValue(response.body(), DeviceDto.class);
|
||||||
|
fsOwnerId.accumulateAndGet(device.name, (s1, s2) -> s1 + Constants.HUB_USER_DEVICE_SEPARATOR + s2);
|
||||||
requestVaultMasterkey(device.userPrivateKey);
|
requestVaultMasterkey(device.userPrivateKey);
|
||||||
}
|
}
|
||||||
case 404 -> Platform.runLater(this::needsDeviceRegistration);
|
case 404 -> Platform.runLater(this::needsDeviceRegistration);
|
||||||
@@ -289,13 +331,16 @@ public class ReceiveKeyController implements FxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String extractVaultId(URI vaultKeyUri) {
|
private static String extractVaultId(URI vaultKeyUri) {
|
||||||
assert vaultKeyUri.getScheme().startsWith(SCHEME_PREFIX);
|
assert vaultKeyUri.getScheme().startsWith(HubKeyLoadingStrategy.SCHEME_PREFIX);
|
||||||
var path = vaultKeyUri.getPath();
|
var path = vaultKeyUri.getPath();
|
||||||
return path.substring(path.lastIndexOf('/') + 1);
|
return path.substring(path.lastIndexOf('/') + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
private record DeviceDto(@JsonProperty(value = "userPrivateKey", required = true) String userPrivateKey) {}
|
private record UserDto(@JsonProperty(value = "name", required = true) String name) {}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
private record DeviceDto(@JsonProperty(value = "name", required = true) String name, @JsonProperty(value = "userPrivateKey", required = true) String userPrivateKey) {}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
private record ConfigDto(@JsonProperty(value = "apiLevel") int apiLevel) {}
|
private record ConfigDto(@JsonProperty(value = "apiLevel") int apiLevel) {}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
|
|||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.common.VaultService;
|
import org.cryptomator.ui.common.VaultService;
|
||||||
import org.cryptomator.ui.dialogs.Dialogs;
|
import org.cryptomator.ui.dialogs.Dialogs;
|
||||||
import org.cryptomator.ui.fxapp.FxFSEventList;
|
|
||||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||||
|
import org.cryptomator.ui.fxapp.FxFSEventList;
|
||||||
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
||||||
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
|
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -139,8 +139,8 @@ public class VaultListController implements FxController {
|
|||||||
vaultList.setItems(vaults);
|
vaultList.setItems(vaults);
|
||||||
vaultList.setCellFactory(cellFactory);
|
vaultList.setCellFactory(cellFactory);
|
||||||
|
|
||||||
vaultList.prefHeightProperty().bind(
|
vaultList.prefHeightProperty().bind( //
|
||||||
vaultList.fixedCellSizeProperty().multiply(Bindings.size(vaultList.getItems()))
|
vaultList.fixedCellSizeProperty().multiply(Bindings.size(vaultList.getItems())) //
|
||||||
);
|
);
|
||||||
|
|
||||||
selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty());
|
selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty());
|
||||||
@@ -157,11 +157,11 @@ public class VaultListController implements FxController {
|
|||||||
//unlock vault on double click
|
//unlock vault on double click
|
||||||
vaultList.addEventFilter(MouseEvent.MOUSE_CLICKED, click -> {
|
vaultList.addEventFilter(MouseEvent.MOUSE_CLICKED, click -> {
|
||||||
if (click.getClickCount() >= 2) {
|
if (click.getClickCount() >= 2) {
|
||||||
Optional.ofNullable(selectedVault.get())
|
Optional.ofNullable(selectedVault.get()) //
|
||||||
.filter(Vault::isLocked)
|
.filter(Vault::isLocked) //
|
||||||
.ifPresent(vault -> appWindows.startUnlockWorkflow(vault, mainWindow));
|
.ifPresent(vault -> appWindows.startUnlockWorkflow(vault, mainWindow));
|
||||||
Optional.ofNullable(selectedVault.get())
|
Optional.ofNullable(selectedVault.get()) //
|
||||||
.filter(Vault::isUnlocked)
|
.filter(Vault::isUnlocked) //
|
||||||
.ifPresent(vaultService::reveal);
|
.ifPresent(vaultService::reveal);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.cryptomator.ui.notification;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
import dagger.Subcomponent;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
@NotificationScoped
|
||||||
|
@Subcomponent(modules = {NotificationModule.class})
|
||||||
|
public interface NotificationComponent {
|
||||||
|
|
||||||
|
@NotificationWindow
|
||||||
|
Stage window();
|
||||||
|
|
||||||
|
@FxmlScene(FxmlFile.NOTIFICATION)
|
||||||
|
Lazy<Scene> scene();
|
||||||
|
|
||||||
|
default Stage showNotification() {
|
||||||
|
var window = window();
|
||||||
|
window.setScene(scene().get());
|
||||||
|
window.sizeToScene();
|
||||||
|
window.show();
|
||||||
|
window.requestFocus();
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subcomponent.Factory
|
||||||
|
interface Factory {
|
||||||
|
NotificationComponent create();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package org.cryptomator.ui.notification;
|
||||||
|
|
||||||
|
import org.cryptomator.common.Constants;
|
||||||
|
import org.cryptomator.common.settings.Settings;
|
||||||
|
import org.cryptomator.cryptofs.event.FileIsInUseEvent;
|
||||||
|
import org.cryptomator.event.VaultEvent;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.fxapp.FxNotificationManager;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleListProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.beans.value.ObservableStringValue;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.FormatStyle;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
|
@NotificationScoped
|
||||||
|
public class NotificationController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(NotificationController.class);
|
||||||
|
private static final DateTimeFormatter LOCAL_TIME_FORMATTER_TEMPLATE = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault());
|
||||||
|
private static final String BUG_MSG = "If you see this message, please report it on the Cryptomator issue tracker.";
|
||||||
|
|
||||||
|
private final Stage window;
|
||||||
|
private final SimpleListProperty<VaultEvent> events;
|
||||||
|
private final ResourceBundle resourceBundle;
|
||||||
|
private final IntegerProperty selectionIndex;
|
||||||
|
private final ObservableStringValue paging;
|
||||||
|
private final ObjectProperty<VaultEvent> selectedEvent;
|
||||||
|
private final ObservableValue<Boolean> singleEvent;
|
||||||
|
private final StringProperty fileName;
|
||||||
|
private final StringProperty vaultName;
|
||||||
|
private final StringProperty eventTimestamp;
|
||||||
|
private final StringProperty message;
|
||||||
|
private final StringProperty description;
|
||||||
|
private final StringProperty actionText;
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
private final DateTimeFormatter localizedTimeFormatter;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public NotificationController(@NotificationWindow Stage window, FxNotificationManager notificationManager, ExecutorService executorService, ResourceBundle resourceBundle, Settings settings) {
|
||||||
|
this.window = window;
|
||||||
|
var preferredLanguage = settings.language.get();
|
||||||
|
this.localizedTimeFormatter = LOCAL_TIME_FORMATTER_TEMPLATE.withLocale(preferredLanguage == null ? Locale.getDefault() : Locale.forLanguageTag(preferredLanguage));
|
||||||
|
this.events = new SimpleListProperty<>(notificationManager.getEventsRequiringNotification());
|
||||||
|
this.resourceBundle = resourceBundle;
|
||||||
|
this.selectionIndex = new SimpleIntegerProperty(-1);
|
||||||
|
this.selectedEvent = new SimpleObjectProperty<>();
|
||||||
|
this.singleEvent = events.sizeProperty().map(size -> size.intValue() == 1);
|
||||||
|
this.paging = Bindings.createStringBinding(() -> selectionIndex.get() + 1 + "/" + events.size(), selectionIndex, events);
|
||||||
|
this.vaultName = new SimpleStringProperty();
|
||||||
|
this.eventTimestamp = new SimpleStringProperty();
|
||||||
|
this.message = new SimpleStringProperty();
|
||||||
|
this.fileName = new SimpleStringProperty("");
|
||||||
|
this.description = new SimpleStringProperty();
|
||||||
|
this.actionText = new SimpleStringProperty();
|
||||||
|
this.executorService = executorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
window.setOnShowing(_ -> selectionIndex.set(0));
|
||||||
|
selectionIndex.addListener((_, _, n) -> {
|
||||||
|
if (!events.isEmpty()) {
|
||||||
|
selectedEvent.setValue(events.get(n.intValue()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
selectedEvent.addListener(this::selectTexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectTexts(ObservableValue<? extends VaultEvent> observable, VaultEvent oldEvent, VaultEvent newEvent) {
|
||||||
|
if (newEvent == null) {
|
||||||
|
vaultName.set("");
|
||||||
|
message.set("NO CONTENT");
|
||||||
|
description.set(BUG_MSG);
|
||||||
|
actionText.set(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vaultName.set(newEvent.v().getDisplayName());
|
||||||
|
switch (newEvent.actualEvent()) {
|
||||||
|
case FileIsInUseEvent fiiue -> {
|
||||||
|
var userAndDevice = fiiue.owner().split(Constants.HUB_USER_DEVICE_SEPARATOR);
|
||||||
|
var user = userAndDevice[0];
|
||||||
|
var device = userAndDevice.length == 1 ? userAndDevice[0] : userAndDevice[1];
|
||||||
|
var cleartextFileName = fiiue.cleartextPath().substring(fiiue.cleartextPath().lastIndexOf('/') + 1);
|
||||||
|
eventTimestamp.set(localizedTimeFormatter.format(fiiue.lastUpdated()));
|
||||||
|
message.set("File is locked by another device");
|
||||||
|
fileName.set(cleartextFileName);
|
||||||
|
description.set("The file is opened by %s on device %s. Ask the user to close the file and sync again. Otherwise, you can ignore the lock and open it anyway.".formatted(user, device));
|
||||||
|
actionText.set("Ignore Lock");
|
||||||
|
/* TODO: Once feature is out of beta, activate translations
|
||||||
|
message.set(resourceBundle.getString("notification.inUse.message"));
|
||||||
|
description.set(resourceBundle.getString("notification.inUse.description").formatted(fiiue.cleartextPath(), user, device));
|
||||||
|
actionText.set(resourceBundle.getString("notification.inUse.action"));
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
message.set("NO CONTENT");
|
||||||
|
description.set(BUG_MSG);
|
||||||
|
actionText.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void processSelectedEvent() {
|
||||||
|
try {
|
||||||
|
var ev = selectedEvent.get();
|
||||||
|
switch (ev.actualEvent()) {
|
||||||
|
case FileIsInUseEvent fiiue -> {
|
||||||
|
executorService.submit(fiiue.ignoreMethod());
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
} //normally nothing
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
removeSelectedEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeSelectedEvent() {
|
||||||
|
int i = selectionIndex.get();
|
||||||
|
var size = events.size();
|
||||||
|
if (i < 0 || i >= size) {
|
||||||
|
LOG.error("Selection index {} is out of bounds of list size {} during event removal. Closing Window.", i, size);
|
||||||
|
window.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
events.remove(i);
|
||||||
|
if (events.isEmpty()) {
|
||||||
|
window.close(); //no more events
|
||||||
|
} else if (events.size() == i) {
|
||||||
|
selectionIndex.set(i - 1); //triggers event update
|
||||||
|
} else {
|
||||||
|
selectedEvent.set(events.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void previousNotification() {
|
||||||
|
int i = selectionIndex.get();
|
||||||
|
if (i != 0) {
|
||||||
|
selectionIndex.set(i - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void nextNotification() {
|
||||||
|
int i = selectionIndex.get();
|
||||||
|
if (i != events.size() - 1) {
|
||||||
|
selectionIndex.set(i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void close() {
|
||||||
|
events.clear();
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//FXML bindings
|
||||||
|
public ObservableValue<String> eventTimeProperty() {
|
||||||
|
return eventTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEventTime() {
|
||||||
|
return eventTimestamp.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableValue<String> vaultNameProperty() {
|
||||||
|
return vaultName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVaultName() {
|
||||||
|
return vaultName.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableValue<String> fileNameProperty() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileName() {
|
||||||
|
return fileName.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableValue<String> messageProperty() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableValue<String> descriptionProperty() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty actionTextProperty() {
|
||||||
|
return actionText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getActionText() {
|
||||||
|
return Objects.requireNonNullElse(actionText.get(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableStringValue pagingProperty() {
|
||||||
|
return paging;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPaging() {
|
||||||
|
return paging.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableValue<Boolean> singleEventProperty() {
|
||||||
|
return singleEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSingleEvent() {
|
||||||
|
return singleEvent.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package org.cryptomator.ui.notification;
|
||||||
|
|
||||||
|
import dagger.Binds;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoMap;
|
||||||
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
|
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxControllerKey;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.cryptomator.ui.common.StageInitializer;
|
||||||
|
import org.cryptomator.ui.common.SystemBarUtil;
|
||||||
|
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Screen;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.StageStyle;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class NotificationModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@NotificationWindow
|
||||||
|
@NotificationScoped
|
||||||
|
static Stage provideStage(StageInitializer initializer) {
|
||||||
|
Stage stage = new Stage(StageStyle.TRANSPARENT);
|
||||||
|
stage.setTitle("Filesystem notification"); //TODO: translate
|
||||||
|
stage.setResizable(false);
|
||||||
|
stage.initModality(Modality.NONE);
|
||||||
|
stage.setAlwaysOnTop(true);
|
||||||
|
initializer.accept(stage);
|
||||||
|
stage.setOnShown(_ -> placeWindow(stage));
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places the notification window on the screen according to some heuristic based on operating system and system bar placement.
|
||||||
|
* <p>
|
||||||
|
* On macOS, the window is placed in the top-right corner of the primary screen, following platform conventions.
|
||||||
|
* On other operating systems, the window placement depends on the location of the system bar:
|
||||||
|
* <ul>
|
||||||
|
* <li>If the system bar is at the top, the window is centered horizontally at the top of the screen.</li>
|
||||||
|
* <li>Otherwise (e.g., system bar at the bottom or elsewhere), the window is placed in the bottom-right corner.</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* The method uses the visual bounds of the primary screen to avoid overlapping with system UI elements.
|
||||||
|
* Assumes the window size has already been set before calling this method.
|
||||||
|
*
|
||||||
|
* @param window the Stage representing the notification window to be placed
|
||||||
|
*/
|
||||||
|
static void placeWindow(Stage window) {
|
||||||
|
var screen = Screen.getPrimary();
|
||||||
|
var vBounds = screen.getVisualBounds();
|
||||||
|
if (SystemUtils.IS_OS_MAC) { //place to right top
|
||||||
|
window.setX(vBounds.getMaxX() - window.getWidth());
|
||||||
|
window.setY(vBounds.getMinY());
|
||||||
|
} else {
|
||||||
|
switch (SystemBarUtil.getPlacementOfSystembar(screen)) {
|
||||||
|
case TOP -> { //place to middle top
|
||||||
|
window.setX(vBounds.getMinX() + (vBounds.getWidth() - window.getWidth()) / 2.0);
|
||||||
|
window.setY(vBounds.getMinY());
|
||||||
|
}
|
||||||
|
default -> { //place to right bottom
|
||||||
|
window.setX(vBounds.getMaxX() - window.getWidth());
|
||||||
|
window.setY(vBounds.getMaxY() - window.getHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// javafx setup
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.NOTIFICATION)
|
||||||
|
@NotificationScoped
|
||||||
|
static Scene provideNotificationScene(@NotificationWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
|
return fxmlLoaders.createScene(FxmlFile.NOTIFICATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@NotificationScoped
|
||||||
|
@NotificationWindow
|
||||||
|
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||||
|
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(NotificationController.class)
|
||||||
|
abstract FxController bindNotificationController(NotificationController controller);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.cryptomator.ui.notification;
|
||||||
|
|
||||||
|
import javax.inject.Scope;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
@Scope
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface NotificationScoped {}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package org.cryptomator.ui.notification;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
public @interface NotificationWindow {}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ abstract class UnlockModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@UnlockWindow
|
@UnlockWindow
|
||||||
@UnlockScoped
|
@UnlockScoped
|
||||||
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @UnlockWindow Vault vault, @UnlockWindow Stage window) {
|
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Factory compFactory, @UnlockWindow Vault vault, @UnlockWindow Stage window) {
|
||||||
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
|
return compFactory.create(vault, window).keyloadingStrategy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,6 +113,11 @@
|
|||||||
-fx-font-size: 1.2em;
|
-fx-font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-window-title {
|
||||||
|
-fx-font-family: 'Open Sans SemiBold';
|
||||||
|
-fx-font-size: 1.0em;
|
||||||
|
}
|
||||||
|
|
||||||
.label-small {
|
.label-small {
|
||||||
-fx-font-size: 0.8em;
|
-fx-font-size: 0.8em;
|
||||||
}
|
}
|
||||||
@@ -1186,3 +1191,32 @@
|
|||||||
-fx-background-color: MAIN_BG;
|
-fx-background-color: MAIN_BG;
|
||||||
-fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */
|
-fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */
|
||||||
}
|
}
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Notification Window
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
.notification-window {
|
||||||
|
-fx-background-color: MAIN_BG;
|
||||||
|
-fx-background-radius: 8px;
|
||||||
|
-fx-border-radius: 8px;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-border-color: MUTED_BG;
|
||||||
|
-fx-border-width: 1px;
|
||||||
|
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 28, 0.35, 0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-window .dialog-header {
|
||||||
|
-fx-alignment: center-left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-window .window-bar-button {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
-fx-padding: 6 8 6 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-window .window-bar-button:armed {
|
||||||
|
-fx-background-color: CONTROL_BG_ARMED;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
}
|
||||||
@@ -113,6 +113,11 @@
|
|||||||
-fx-font-size: 1.2em;
|
-fx-font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-window-title {
|
||||||
|
-fx-font-family: 'Open Sans SemiBold';
|
||||||
|
-fx-font-size: 1.0em;
|
||||||
|
}
|
||||||
|
|
||||||
.label-small {
|
.label-small {
|
||||||
-fx-font-size: 0.8em;
|
-fx-font-size: 0.8em;
|
||||||
}
|
}
|
||||||
@@ -1186,3 +1191,33 @@
|
|||||||
-fx-background-color: MAIN_BG;
|
-fx-background-color: MAIN_BG;
|
||||||
-fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */
|
-fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Notification Window
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
.notification-window {
|
||||||
|
-fx-background-color: MAIN_BG;
|
||||||
|
-fx-background-radius: 8px;
|
||||||
|
-fx-border-radius: 8px;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-border-color: MUTED_BG;
|
||||||
|
-fx-border-width: 1px;
|
||||||
|
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 28, 0.35, 0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-window .dialog-header {
|
||||||
|
-fx-alignment: center-left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-window .window-bar-button {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
-fx-padding: 6 8 6 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-window .window-bar-button:armed {
|
||||||
|
-fx-background-color: CONTROL_BG_ARMED;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
}
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
<CheckBox fx:id="expertSettingsCheckBox" text="%addvaultwizard.new.expertSettings.enableExpertSettingsCheckbox" onAction="#toggleUseExpertSettings"/>
|
<CheckBox fx:id="expertSettingsCheckBox" text="%addvaultwizard.new.expertSettings.enableExpertSettingsCheckbox" onAction="#toggleUseExpertSettings"/>
|
||||||
<VBox spacing="6" visible="${expertSettingsCheckBox.selected}">
|
<VBox spacing="6" visible="${expertSettingsCheckBox.selected}">
|
||||||
<HBox spacing="2" HBox.hgrow="NEVER">
|
<HBox spacing="2" HBox.hgrow="NEVER">
|
||||||
<Label text="%addvaultwizard.new.expertSettings.shorteningThreshold.title"/>
|
<Label text="%addvaultwizard.new.expertSettings.shorteningThreshold.title" labelFor="$shorteningThresholdTextField"/>
|
||||||
<Region prefWidth="2"/>
|
<Region prefWidth="2"/>
|
||||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs">
|
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs" accessibleText="%addvaultwizard.new.expertSettings.shorteningThreshold.tooltip">
|
||||||
<graphic>
|
<graphic>
|
||||||
<FontAwesome5IconView glyph="QUESTION_CIRCLE" styleClass="glyph-icon-muted"/>
|
<FontAwesome5IconView glyph="QUESTION_CIRCLE" styleClass="glyph-icon-muted"/>
|
||||||
</graphic>
|
</graphic>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</padding>
|
</padding>
|
||||||
<children>
|
<children>
|
||||||
<VBox spacing="6">
|
<VBox spacing="6">
|
||||||
<FormattedLabel format="%changepassword.enterOldPassword" arg1="${controller.vault.displayName}" wrapText="true"/>
|
<FormattedLabel format="%changepassword.enterOldPassword" arg1="${controller.vault.displayName}" wrapText="true" labelFor="$oldPasswordField"/>
|
||||||
<NiceSecurePasswordField fx:id="oldPasswordField"/>
|
<NiceSecurePasswordField fx:id="oldPasswordField"/>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<Insets left="6"/>
|
<Insets left="6"/>
|
||||||
</padding>
|
</padding>
|
||||||
<Region HBox.hgrow="ALWAYS"/>
|
<Region HBox.hgrow="ALWAYS"/>
|
||||||
<Button styleClass="button-right" contentDisplay="GRAPHIC_ONLY" onAction="#copyTableToClipboard">
|
<Button styleClass="button-right" contentDisplay="GRAPHIC_ONLY" onAction="#copyTableToClipboard" accessibleText="%decryptNames.copyTable.tooltip">
|
||||||
<graphic>
|
<graphic>
|
||||||
<FontAwesome5IconView glyph="CLIPBOARD" glyphSize="16"/>
|
<FontAwesome5IconView glyph="CLIPBOARD" glyphSize="16"/>
|
||||||
</graphic>
|
</graphic>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<Tooltip text="%decryptNames.copyTable.tooltip"/>
|
<Tooltip text="%decryptNames.copyTable.tooltip"/>
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
<Button styleClass="button-right" contentDisplay="GRAPHIC_ONLY" onAction="#clearTable">
|
<Button styleClass="button-right" contentDisplay="GRAPHIC_ONLY" onAction="#clearTable" accessibleText="%decryptNames.clearTable.tooltip">
|
||||||
<graphic>
|
<graphic>
|
||||||
<FontAwesome5IconView glyph="TRASH" glyphSize="16"/>
|
<FontAwesome5IconView glyph="TRASH" glyphSize="16"/>
|
||||||
</graphic>
|
</graphic>
|
||||||
@@ -48,12 +48,12 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</placeholder>
|
</placeholder>
|
||||||
<columns>
|
<columns>
|
||||||
<TableColumn fx:id="ciphertextColumn" prefWidth="${cipherToCleartextTable.width * 0.5}">
|
<TableColumn fx:id="ciphertextColumn" prefWidth="${cipherToCleartextTable.width * 0.5}" text="%decryptNames.column.encrypted">
|
||||||
<graphic>
|
<graphic>
|
||||||
<FontAwesome5IconView glyph="LOCK"/>
|
<FontAwesome5IconView glyph="LOCK"/>
|
||||||
</graphic>
|
</graphic>
|
||||||
</TableColumn>
|
</TableColumn>
|
||||||
<TableColumn fx:id="cleartextColumn" prefWidth="${cipherToCleartextTable.width * 0.5}">
|
<TableColumn fx:id="cleartextColumn" prefWidth="${cipherToCleartextTable.width * 0.5}" text="%decryptNames.column.decrypted">
|
||||||
<graphic>
|
<graphic>
|
||||||
<FontAwesome5IconView glyph="LOCK_OPEN"/>
|
<FontAwesome5IconView glyph="LOCK_OPEN"/>
|
||||||
</graphic>
|
</graphic>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
<padding>
|
<padding>
|
||||||
<Insets top="6" bottom="6"/>
|
<Insets top="6" bottom="6"/>
|
||||||
</padding>
|
</padding>
|
||||||
<Label text="%error.technicalDetails"/>
|
<Label text="%error.technicalDetails" labelFor="$detailsTextArea"/>
|
||||||
<Region HBox.hgrow="ALWAYS"/>
|
<Region HBox.hgrow="ALWAYS"/>
|
||||||
<Hyperlink styleClass="hyperlink-underline" text="%generic.button.copy" onAction="#copyDetails" contentDisplay="LEFT" visible="${!controller.copiedDetails}" managed="${!controller.copiedDetails}">
|
<Hyperlink styleClass="hyperlink-underline" text="%generic.button.copy" onAction="#copyDetails" contentDisplay="LEFT" visible="${!controller.copiedDetails}" managed="${!controller.copiedDetails}">
|
||||||
<graphic>
|
<graphic>
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
</graphic>
|
</graphic>
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
</HBox>
|
</HBox>
|
||||||
<TextArea VBox.vgrow="ALWAYS" text="${controller.detailText}" prefRowCount="5" editable="false"/>
|
<TextArea fx:id="detailsTextArea" VBox.vgrow="ALWAYS" text="${controller.detailText}" prefRowCount="5" editable="false"/>
|
||||||
|
|
||||||
<Region minHeight="18"/>
|
<Region minHeight="18"/>
|
||||||
<ButtonBar buttonMinWidth="120" buttonOrder="B+C">
|
<ButtonBar buttonMinWidth="120" buttonOrder="B+C">
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
<padding>
|
<padding>
|
||||||
<Insets left="6" />
|
<Insets left="6" />
|
||||||
</padding>
|
</padding>
|
||||||
<ChoiceBox fx:id="vaultFilterChoiceBox" minWidth="42"/>
|
<ChoiceBox fx:id="vaultFilterChoiceBox" minWidth="42" accessibleText="%eventView.filterVaults"/>
|
||||||
<Region HBox.hgrow="ALWAYS"/>
|
<Region HBox.hgrow="ALWAYS"/>
|
||||||
<Button styleClass="button-right" onAction="#clearEvents" contentDisplay="GRAPHIC_ONLY">
|
<Button styleClass="button-right" onAction="#clearEvents" contentDisplay="GRAPHIC_ONLY" accessibleText="%eventView.clearListButton.tooltip">
|
||||||
<graphic>
|
<graphic>
|
||||||
<FontAwesome5IconView glyph="TRASH" glyphSize="16"/>
|
<FontAwesome5IconView glyph="TRASH" glyphSize="16"/>
|
||||||
</graphic>
|
</graphic>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<?import javafx.scene.control.Button?>
|
<?import javafx.scene.control.Button?>
|
||||||
<?import javafx.scene.control.ContextMenu?>
|
<?import javafx.scene.control.ContextMenu?>
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.Tooltip?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<HBox xmlns:fx="http://javafx.com/fxml"
|
<HBox xmlns:fx="http://javafx.com/fxml"
|
||||||
@@ -29,10 +30,13 @@
|
|||||||
</HBox>
|
</HBox>
|
||||||
<Label text="${controller.description}"/>
|
<Label text="${controller.description}"/>
|
||||||
</VBox>
|
</VBox>
|
||||||
<Button fx:id="eventActionsButton" contentDisplay="GRAPHIC_ONLY" onAction="#toggleEventActionsMenu" managed="${controller.actionsButtonVisible}" visible="${controller.actionsButtonVisible}">
|
<Button fx:id="eventActionsButton" contentDisplay="GRAPHIC_ONLY" onAction="#toggleEventActionsMenu" managed="${controller.actionsButtonVisible}" visible="${controller.actionsButtonVisible}" accessibleText="%eventView.cell.actionsButton.tooltip">
|
||||||
<graphic>
|
<graphic>
|
||||||
<FontAwesome5IconView glyph="ELLIPSIS_V" glyphSize="16"/>
|
<FontAwesome5IconView glyph="ELLIPSIS_V" glyphSize="16"/>
|
||||||
</graphic>
|
</graphic>
|
||||||
|
<tooltip>
|
||||||
|
<Tooltip text="%eventView.cell.actionsButton.tooltip"/>
|
||||||
|
</tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
<VBox alignment="CENTER" maxWidth="64" minWidth="64" visible="${!controller.actionsButtonVisible}" managed="${!controller.actionsButtonVisible}">
|
<VBox alignment="CENTER" maxWidth="64" minWidth="64" visible="${!controller.actionsButtonVisible}" managed="${!controller.actionsButtonVisible}">
|
||||||
<Label text="${controller.eventLocalTime}" />
|
<Label text="${controller.eventLocalTime}" />
|
||||||
|
|||||||
@@ -47,8 +47,8 @@
|
|||||||
<FontAwesome5IconView glyph="FUNNEL" glyphSize="${filterLbl.height}" styleClass="glyph-icon-muted"/>
|
<FontAwesome5IconView glyph="FUNNEL" glyphSize="${filterLbl.height}" styleClass="glyph-icon-muted"/>
|
||||||
</graphic>
|
</graphic>
|
||||||
</Label>
|
</Label>
|
||||||
<ChoiceBox fx:id="severityChoiceBox" />
|
<ChoiceBox fx:id="severityChoiceBox" accessibleText="%health.check.detail.filterSeverity"/>
|
||||||
<ChoiceBox fx:id="fixStateChoiceBox" />
|
<ChoiceBox fx:id="fixStateChoiceBox" accessibleText="%health.check.detail.filterFixState"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS" visible="${!controller.checkSkipped}" fixedCellSize="25">
|
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS" visible="${!controller.checkSkipped}" fixedCellSize="25">
|
||||||
<contextMenu>
|
<contextMenu>
|
||||||
|
|||||||
@@ -21,6 +21,6 @@
|
|||||||
<CheckBox fx:id="checkbox" visible="${controller.checkRunnable}"/>
|
<CheckBox fx:id="checkbox" visible="${controller.checkRunnable}"/>
|
||||||
<CheckStateIconView check="${controller.check}" glyphSize="20" visible="${!controller.checkRunnable}"/>
|
<CheckStateIconView check="${controller.check}" glyphSize="20" visible="${!controller.checkRunnable}"/>
|
||||||
</StackPane>
|
</StackPane>
|
||||||
<Label text="${controller.checkName}"/>
|
<Label text="${controller.checkName}" labelFor="$checkbox"/>
|
||||||
|
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</padding>
|
</padding>
|
||||||
<children>
|
<children>
|
||||||
<VBox spacing="6" visible="${!controller.vault.processing}" managed="${!controller.vault.processing}">
|
<VBox spacing="6" visible="${!controller.vault.processing}" managed="${!controller.vault.processing}">
|
||||||
<FormattedLabel format="%migration.run.enterPassword" arg1="${controller.vault.displayName}" wrapText="true"/>
|
<FormattedLabel format="%migration.run.enterPassword" arg1="${controller.vault.displayName}" wrapText="true" labelFor="$passwordField"/>
|
||||||
<NiceSecurePasswordField fx:id="passwordField"/>
|
<NiceSecurePasswordField fx:id="passwordField"/>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.Separator?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.image.Image?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.layout.BorderPane?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.Region?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import javafx.scene.control.ScrollPane?>
|
||||||
|
<BorderPane xmlns="http://javafx.com/javafx"
|
||||||
|
xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="org.cryptomator.ui.notification.NotificationController"
|
||||||
|
prefHeight="200.0" prefWidth="400.0" maxHeight="200.0" maxWidth="400.0"
|
||||||
|
styleClass="notification-window">
|
||||||
|
<padding>
|
||||||
|
<Insets top="12" right="12" bottom="12" left="12"/>
|
||||||
|
</padding>
|
||||||
|
<top>
|
||||||
|
<VBox >
|
||||||
|
<HBox spacing="6" styleClass="dialog-header" alignment="CENTER_LEFT">
|
||||||
|
<ImageView fitHeight="12" preserveRatio="true" cache="true">
|
||||||
|
<Image url="@../img/logo64.png"/>
|
||||||
|
</ImageView>
|
||||||
|
<Label text="Cryptomator" styleClass="label-window-title"/>
|
||||||
|
<Region HBox.hgrow="ALWAYS"/>
|
||||||
|
<HBox styleClass="dialog-header" alignment="CENTER_LEFT" visible="${!controller.singleEvent}">
|
||||||
|
<Button contentDisplay="GRAPHIC_ONLY" styleClass="window-bar-button" onAction="#previousNotification" accessibleText="%generic.button.previous">
|
||||||
|
<graphic>
|
||||||
|
<FontAwesome5IconView glyph="CHEVRON_LEFT" glyphSize="12"/>
|
||||||
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
<Label text="${controller.paging}" styleClass="label-window-title"/>
|
||||||
|
<Button contentDisplay="GRAPHIC_ONLY" styleClass="window-bar-button" onAction="#nextNotification" accessibleText="%generic.button.next">
|
||||||
|
<graphic>
|
||||||
|
<FontAwesome5IconView glyph="CHEVRON_RIGHT" glyphSize="12"/>
|
||||||
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
</HBox>
|
||||||
|
<Button contentDisplay="GRAPHIC_ONLY" onAction="#close" styleClass="window-bar-button" accessibleText="%generic.button.close">
|
||||||
|
<graphic>
|
||||||
|
<FontAwesome5IconView glyph="TIMES" glyphSize="12"/>
|
||||||
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
</HBox>
|
||||||
|
<Separator orientation="HORIZONTAL"/>
|
||||||
|
</VBox>
|
||||||
|
</top>
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<VBox>
|
||||||
|
<padding>
|
||||||
|
<Insets top="6"/>
|
||||||
|
</padding>
|
||||||
|
<HBox>
|
||||||
|
<Label text="${controller.vaultName}" styleClass="label-small" wrapText="true"/>
|
||||||
|
<Region minWidth="12" HBox.hgrow="ALWAYS"/>
|
||||||
|
<Label text="${controller.eventTime}" styleClass="label-small" />
|
||||||
|
</HBox>
|
||||||
|
<Label text="${controller.message}" styleClass="label-large" wrapText="true"/>
|
||||||
|
<Label text="${controller.fileName}" styleClass="label" textOverrun="CENTER_ELLIPSIS" visible="${!controller.fileName.empty}" managed="${!controller.fileName.empty}"/>
|
||||||
|
<Region minHeight="6"/>
|
||||||
|
<ScrollPane minViewportWidth="370" minViewportHeight="50">
|
||||||
|
<Label text="${controller.description}" styleClass="label" wrapText="true" maxWidth="370"/>
|
||||||
|
</ScrollPane>
|
||||||
|
<Region VBox.vgrow="ALWAYS"/>
|
||||||
|
<Button text="${controller.actionText}" onAction="#processSelectedEvent"
|
||||||
|
visible="${!controller.actionText.empty}" managed="${!controller.actionText.empty}"/>
|
||||||
|
</VBox>
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user