mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-22 07:54:19 +00:00
Compare commits
411
Commits
1.5.10
...
1.6.0-alpha2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fce262acb | ||
|
|
1159681824 | ||
|
|
c868ac32cc | ||
|
|
f85f953668 | ||
|
|
5185df631d | ||
|
|
617feec111 | ||
|
|
eb196165c5 | ||
|
|
8c37ab3675 | ||
|
|
7eaade1555 | ||
|
|
186c129a30 | ||
|
|
f74b9d3347 | ||
|
|
6038559172 | ||
|
|
98a899c7e2 | ||
|
|
783c8718c1 | ||
|
|
b47382811f | ||
|
|
62f279b9f7 | ||
|
|
ab961f1a8b | ||
|
|
9562db7e13 | ||
|
|
1ca2e029dc | ||
|
|
944e3e6c50 | ||
|
|
e134d2f422 | ||
|
|
04e5170403 | ||
|
|
ea008df9f7 | ||
|
|
e25dfaad35 | ||
|
|
e98f92c63f | ||
|
|
85b761a9d7 | ||
|
|
be8dcd4dc4 | ||
|
|
c01c405f20 | ||
|
|
b4f268f551 | ||
|
|
21f9e72cc5 | ||
|
|
4845984a35 | ||
|
|
b6f5ddaf30 | ||
|
|
1abf0e1bfa | ||
|
|
a8364a8c1a | ||
|
|
13c95868fa | ||
|
|
85ff7542f1 | ||
|
|
432be6dd80 | ||
|
|
ca0402aaf0 | ||
|
|
c7ef0056f9 | ||
|
|
cf13311665 | ||
|
|
a7133dbebe | ||
|
|
168f9b9fb9 | ||
|
|
2b332c3831 | ||
|
|
66e3625a0b | ||
|
|
20f88e203a | ||
|
|
66a8dc9811 | ||
|
|
9d89efc98c | ||
|
|
dfbe017316 | ||
|
|
d2680b9857 | ||
|
|
8e1be4c67b | ||
|
|
749e0ac02d | ||
|
|
a2ad7d69ab | ||
|
|
a47c05c87d | ||
|
|
6b871e56c8 | ||
|
|
40dbc0ba48 | ||
|
|
2e52afed51 | ||
|
|
64d54133f8 | ||
|
|
ac289cc3f7 | ||
|
|
189699d359 | ||
|
|
8e5035f506 | ||
|
|
0ce41e7ac3 | ||
|
|
c1a249fd7f | ||
|
|
c266c7583b | ||
|
|
00331d4857 | ||
|
|
8fd484e2bb | ||
|
|
165d740acd | ||
|
|
fd4010c6c9 | ||
|
|
aa2900fa9e | ||
|
|
76d1875e01 | ||
|
|
bc83e23a34 | ||
|
|
6a326bcbce | ||
|
|
573ad03d68 | ||
|
|
07efb3e94d | ||
|
|
89e680fb45 | ||
|
|
54128a0016 | ||
|
|
e2565097cd | ||
|
|
f827b1fc89 | ||
|
|
c0baab2329 | ||
|
|
70baa4d09f | ||
|
|
d710d406d0 | ||
|
|
554f245f5d | ||
|
|
34995088ba | ||
|
|
69c63702f2 | ||
|
|
db7e731554 | ||
|
|
e08b43bf17 | ||
|
|
58b45bacd1 | ||
|
|
4f942bc23f | ||
|
|
09a8618fa0 | ||
|
|
52af530d1b | ||
|
|
86a42234c6 | ||
|
|
37c4e78b1d | ||
|
|
9c2c234bee | ||
|
|
59f91267ae | ||
|
|
b3ff9423b4 | ||
|
|
e3f3090341 | ||
|
|
83be6c0864 | ||
|
|
c654951dc1 | ||
|
|
4e177c9ea7 | ||
|
|
dc3a951a1b | ||
|
|
defa9c75eb | ||
|
|
c1f498a114 | ||
|
|
8457c50ebc | ||
|
|
0144cbb99f | ||
|
|
d6e4c7d177 | ||
|
|
3376b16b7b | ||
|
|
673fdcd095 | ||
|
|
0d00520ac1 | ||
|
|
22a0d3a9a5 | ||
|
|
24baa44e70 | ||
|
|
03886f88e8 | ||
|
|
642816b631 | ||
|
|
f3a03c71ec | ||
|
|
1fab246fcd | ||
|
|
cd5c55aad7 | ||
|
|
b066b4b045 | ||
|
|
41d2a2c77e | ||
|
|
69186b916c | ||
|
|
0840695e0a | ||
|
|
8447f105b0 | ||
|
|
c306d8df04 | ||
|
|
beba6490c3 | ||
|
|
fb1078b35b | ||
|
|
be0912e6ca | ||
|
|
13db1aa9b9 | ||
|
|
d2cd2c923d | ||
|
|
c1000d9958 | ||
|
|
2810c044ea | ||
|
|
e09bd160b7 | ||
|
|
e6d1e4697c | ||
|
|
497e5ce73f | ||
|
|
cf7cbae567 | ||
|
|
803f517c62 | ||
|
|
0afa7b8e37 | ||
|
|
1a0f70f8e8 | ||
|
|
8737eb83f0 | ||
|
|
a96239a19f | ||
|
|
2b7cfcd1dc | ||
|
|
ebccb61750 | ||
|
|
285f2aec23 | ||
|
|
217e31fbd7 | ||
|
|
f0ebf7a638 | ||
|
|
c05c5e3f90 | ||
|
|
30f0c5e697 | ||
|
|
f989b8627c | ||
|
|
ff84230566 | ||
|
|
3b4f6276b5 | ||
|
|
2be7a050a4 | ||
|
|
96a612127c | ||
|
|
0bdfb7c9f9 | ||
|
|
f556301249 | ||
|
|
f77b237e59 | ||
|
|
61b7a39aad | ||
|
|
16bd84ee62 | ||
|
|
3d0a97fcdf | ||
|
|
786d156b9f | ||
|
|
629b6fb97d | ||
|
|
17dc32bb79 | ||
|
|
c05e00d32a | ||
|
|
9dc8b2cb47 | ||
|
|
0e32e96c7d | ||
|
|
97afadd7b9 | ||
|
|
b199b65e38 | ||
|
|
999abf3c13 | ||
|
|
2cdde54db6 | ||
|
|
6941603cdd | ||
|
|
cdca4e047d | ||
|
|
31e938de6a | ||
|
|
4c10ab764a | ||
|
|
4e075ab0ca | ||
|
|
f73ae9759f | ||
|
|
f6283b2f7e | ||
|
|
b23d2e4def | ||
|
|
91a2943599 | ||
|
|
e32ce22d24 | ||
|
|
e75c415b46 | ||
|
|
0ab28602d1 | ||
|
|
62c8edff04 | ||
|
|
d01c6268f8 | ||
|
|
e083fd0bcc | ||
|
|
ff687f1bae | ||
|
|
557aaa2480 | ||
|
|
78e43d401d | ||
|
|
7b08c5d287 | ||
|
|
84d2644f19 | ||
|
|
f9a9d7b870 | ||
|
|
d75a8c4bba | ||
|
|
d326c7c990 | ||
|
|
e8e63ca4fc | ||
|
|
6878f2e94b | ||
|
|
a17b416262 | ||
|
|
fed572694f | ||
|
|
fe3d67d937 | ||
|
|
3dcdeb1033 | ||
|
|
0898158c5a | ||
|
|
81091d4bdf | ||
|
|
c9b1b1baa5 | ||
|
|
c81f3bd972 | ||
|
|
9f86b74320 | ||
|
|
2d8390a0f3 | ||
|
|
8d3671175f | ||
|
|
366011a222 | ||
|
|
c0c2e21055 | ||
|
|
63be96a151 | ||
|
|
fc1df4bd99 | ||
|
|
7657330931 | ||
|
|
4e44de3c3b | ||
|
|
8d902a72ca | ||
|
|
7378031a72 | ||
|
|
f898c07c27 | ||
|
|
c8f02d1045 | ||
|
|
4cc13b986b | ||
|
|
af7b6780f3 | ||
|
|
4a17d01004 | ||
|
|
1d68408e21 | ||
|
|
821b1ee660 | ||
|
|
c6616bd467 | ||
|
|
4bb2589f16 | ||
|
|
896c3b3565 | ||
|
|
6ee717a693 | ||
|
|
07ec858e1e | ||
|
|
a7c1e43ebc | ||
|
|
ac1cd3b073 | ||
|
|
7474519faf | ||
|
|
340e474c33 | ||
|
|
c90ab75427 | ||
|
|
848b3f77d3 | ||
|
|
fb8cdb31ca | ||
|
|
ba5f8053fb | ||
|
|
38165becca | ||
|
|
f731ac5c90 | ||
|
|
0291702736 | ||
|
|
ab63929861 | ||
|
|
ca36fb8af3 | ||
|
|
8cb29ff83b | ||
|
|
304fa67dd7 | ||
|
|
a80ec32d2c | ||
|
|
dc4f21162e | ||
|
|
3b8d7e0241 | ||
|
|
6891c68cf1 | ||
|
|
18b26fbc58 | ||
|
|
1db8da6b6b | ||
|
|
14bce7e91d | ||
|
|
2674bc6f23 | ||
|
|
74e1d6f991 | ||
|
|
25dba028f8 | ||
|
|
bf51cc13d1 | ||
|
|
24be36cccb | ||
|
|
797e15c775 | ||
|
|
2395dbcae9 | ||
|
|
efacbbe2b6 | ||
|
|
2518086e46 | ||
|
|
c733247611 | ||
|
|
4f44e71e13 | ||
|
|
02aa46ece8 | ||
|
|
635f64c847 | ||
|
|
0a8516ae9a | ||
|
|
c1edb30472 | ||
|
|
93c78b63ca | ||
|
|
3ccae93757 | ||
|
|
450dec2baf | ||
|
|
36121527ca | ||
|
|
f30451f7e1 | ||
|
|
74d7f63f64 | ||
|
|
350a4ab762 | ||
|
|
0e0e75b89c | ||
|
|
e76440d928 | ||
|
|
0cbace67a5 | ||
|
|
84a3082589 | ||
|
|
cdfcca06d1 | ||
|
|
630bfa4cee | ||
|
|
90cb7beb1f | ||
|
|
d37fa33278 | ||
|
|
eed9c906cc | ||
|
|
2b7ed5f4dd | ||
|
|
6a7a309a2b | ||
|
|
0382069995 | ||
|
|
7c5f40d7d1 | ||
|
|
09e9530660 | ||
|
|
947d25b335 | ||
|
|
80cbc6699a | ||
|
|
da0933fa92 | ||
|
|
9ad217ca55 | ||
|
|
adc9f32fb3 | ||
|
|
65369bdbff | ||
|
|
d18e618ef1 | ||
|
|
970216dd59 | ||
|
|
6840a649c7 | ||
|
|
1a0bc92bde | ||
|
|
e4709ed6fe | ||
|
|
21038212c1 | ||
|
|
b15471b4ff | ||
|
|
19ebc7e562 | ||
|
|
0fa052d2d1 | ||
|
|
402861b9c0 | ||
|
|
aca1666dea | ||
|
|
ff17b60f56 | ||
|
|
3284578445 | ||
|
|
70f6a4877c | ||
|
|
85c5dc8dfb | ||
|
|
4b670a59a3 | ||
|
|
23c113948f | ||
|
|
d86ea20c31 | ||
|
|
3f928cf958 | ||
|
|
01929c2288 | ||
|
|
2634433b2c | ||
|
|
c1f44f76b9 | ||
|
|
b2f27c0a3d | ||
|
|
b1d7cfc81b | ||
|
|
7cd5c66836 | ||
|
|
95cef34234 | ||
|
|
8977440697 | ||
|
|
d5eb84a000 | ||
|
|
81fb6b8794 | ||
|
|
92b390d5bb | ||
|
|
8867532210 | ||
|
|
77db435b4f | ||
|
|
cb2bc17283 | ||
|
|
aaa93239d0 | ||
|
|
93445e22d4 | ||
|
|
64897cfa21 | ||
|
|
7349a29831 | ||
|
|
73554b4759 | ||
|
|
0312f045aa | ||
|
|
1134c1b2ff | ||
|
|
efebbc059a | ||
|
|
6e860d293a | ||
|
|
8bb925fd93 | ||
|
|
ca5d9eba36 | ||
|
|
1a49e24d8a | ||
|
|
24b0ed2502 | ||
|
|
a226d5403a | ||
|
|
0f3085cc73 | ||
|
|
f0a040cf29 | ||
|
|
0676748dde | ||
|
|
589b8384eb | ||
|
|
9e3947f337 | ||
|
|
790cc4e772 | ||
|
|
72f4988632 | ||
|
|
b3fa6bbf32 | ||
|
|
c2dc487c79 | ||
|
|
e74dd3be9e | ||
|
|
1eeee61572 | ||
|
|
984b7a2d0e | ||
|
|
45c714a123 | ||
|
|
96bb97d50a | ||
|
|
046acb9bbf | ||
|
|
ce4ec16c62 | ||
|
|
96c8c9a9a9 | ||
|
|
44db97327d | ||
|
|
f441700c25 | ||
|
|
76b023bdbd | ||
|
|
241d9781bb | ||
|
|
ff94532560 | ||
|
|
e949023321 | ||
|
|
da31407d63 | ||
|
|
8c7c5da89a | ||
|
|
e3a0e1ec5a | ||
|
|
4cee27d8dd | ||
|
|
b0fce66d77 | ||
|
|
bf47ba0145 | ||
|
|
fc7b484c9a | ||
|
|
4402c19d90 | ||
|
|
4e89de9e47 | ||
|
|
a27c9d63d9 | ||
|
|
da6aa0f002 | ||
|
|
0bf3533f45 | ||
|
|
295cb0ef6d | ||
|
|
6fac84e9c2 | ||
|
|
77b2e67c49 | ||
|
|
c48c9b1568 | ||
|
|
6da7eae2f8 | ||
|
|
aec56c48c5 | ||
|
|
c0a9a95e4f | ||
|
|
3b73544766 | ||
|
|
fbc471635a | ||
|
|
3c5b9ed3a6 | ||
|
|
8479122561 | ||
|
|
91c883d5cc | ||
|
|
1cd4da0796 | ||
|
|
935aaccdae | ||
|
|
ab3dd779d2 | ||
|
|
1fbcf6d517 | ||
|
|
82538091c9 | ||
|
|
62ade6113b | ||
|
|
4779bbf415 | ||
|
|
c6b786a771 | ||
|
|
6381227897 | ||
|
|
e82167b5e6 | ||
|
|
dc4fd482b5 | ||
|
|
a71f8b350e | ||
|
|
c9ec8fecef | ||
|
|
e72d32f2d1 | ||
|
|
2545aa2a7e | ||
|
|
51aac15622 | ||
|
|
afa0cfeafb | ||
|
|
19e24ba12c | ||
|
|
c47ce2c730 | ||
|
|
5d00b3dd76 | ||
|
|
4db57cc0dc | ||
|
|
432a9a27f1 | ||
|
|
57bfa3276d | ||
|
|
02fc9b263a | ||
|
|
4c66f81736 | ||
|
|
2de151aebe | ||
|
|
c44911dcac | ||
|
|
bcf2a3d20c | ||
|
|
422ce4a387 | ||
|
|
21387bd76c | ||
|
|
52cd560cb2 | ||
|
|
6c440dfbbb | ||
|
|
a074450452 |
@@ -5,18 +5,12 @@ labels: type:bug
|
|||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
⚠️⚠️⚠️ READ CAREFULLY ⚠️⚠️⚠️
|
Please make sure to:
|
||||||
|
- Comply with our code of conduct: https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md
|
||||||
|
- Search for existing similar issues first: https://github.com/cryptomator/cryptomator/issues?q=
|
||||||
|
|
||||||
Do you want to ask a QUESTION? Are you looking for SUPPORT?
|
⚠️ IMPORTANT: If you don't stick to this template, the issue will get closed.
|
||||||
We're happy to help you via our support channels! Please read: https://github.com/cryptomator/cryptomator/blob/develop/SUPPORT.md
|
|
||||||
|
|
||||||
By filing an issue, you are expected to comply with our code of conduct: https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md
|
|
||||||
|
|
||||||
Of course, we also expect you to search for existing similar issues first! ;) https://github.com/cryptomator/cryptomator/issues?q=
|
|
||||||
|
|
||||||
⚠️ IMPORTANT: If you don't stick to this template, the issue will get closed. To proof that you read this, please remove the X from the following line:
|
|
||||||
-->
|
-->
|
||||||
<!-- oooXooo -->
|
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
||||||
@@ -24,7 +18,7 @@ Of course, we also expect you to search for existing similar issues first! ;) ht
|
|||||||
|
|
||||||
### System Setup
|
### System Setup
|
||||||
|
|
||||||
* Operating system and version: [Windows/macOS/Linux + Version]
|
* Operating system and version: [Windows/macOS/Linux + Version ( + Desktop Environment, if Linux)]
|
||||||
* Cryptomator version: [Shown in the settings]
|
* Cryptomator version: [Shown in the settings]
|
||||||
* Volume type: [Dokany/FUSE/WebDAV, shown in the settings]
|
* Volume type: [Dokany/FUSE/WebDAV, shown in the settings]
|
||||||
|
|
||||||
@@ -51,7 +45,6 @@ Of course, we also expect you to search for existing similar issues first! ;) ht
|
|||||||
[Any additional information, log files, screenshots, configuration, or data that might be necessary to reproduce the issue.]
|
[Any additional information, log files, screenshots, configuration, or data that might be necessary to reproduce the issue.]
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
If you want to add the log file or screenshots, please add them as attachments. If your log file seems empty and doesn't show any errors, you may enable the debug mode first. Here is how to do that: https://community.cryptomator.org/t/how-do-i-enable-debug-mode/36
|
If you want to add the log file or screenshots, please add them as attachments. If your log file seems empty and doesn't show any errors, you may enable the debug mode first. Here is how to do that: https://community.cryptomator.org/t/how-do-i-enable-debug-mode/36
|
||||||
|
|
||||||
Then reproduce the problem to ensure all important information is contained in there. You may use test data or redact sensitive information from the log file.
|
Then reproduce the problem to ensure all important information is contained in there. You may use test data or redact sensitive information from the log file.
|
||||||
@@ -60,5 +53,4 @@ Log file location:
|
|||||||
- Windows: %appdata%/Cryptomator
|
- Windows: %appdata%/Cryptomator
|
||||||
- macOS: ~/Library/Logs/Cryptomator
|
- macOS: ~/Library/Logs/Cryptomator
|
||||||
- Linux: ~/.local/share/Cryptomator/logs
|
- Linux: ~/.local/share/Cryptomator/logs
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Help & Support
|
||||||
|
url: https://community.cryptomator.org/
|
||||||
|
about: You will find answers in our community forum
|
||||||
|
- name: User Manual
|
||||||
|
url: https://docs.cryptomator.org/
|
||||||
|
about: Read the Cryptomator documentation here
|
||||||
@@ -5,14 +5,9 @@ labels: type:feature-request
|
|||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
Please make sure to:
|
||||||
Do you want to ask a QUESTION? Are you looking for SUPPORT?
|
- Comply with our code of conduct: https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md
|
||||||
We're happy to help you via our support channels! Please read: https://github.com/cryptomator/cryptomator/blob/develop/SUPPORT.md
|
- Search for existing similar issues first: https://github.com/cryptomator/cryptomator/issues?q=
|
||||||
|
|
||||||
By filing a feature request, you are expected to comply with our code of conduct: https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md
|
|
||||||
|
|
||||||
Of course, we also expect you to search for existing similar feature requests first! ;)
|
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
@@ -29,4 +24,4 @@ Of course, we also expect you to search for existing similar feature requests fi
|
|||||||
|
|
||||||
### Additional Context
|
### Additional Context
|
||||||
|
|
||||||
[Add any other context or screenshots about the feature request here.]
|
[Add any other context or screenshots about the feature request here.]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-java@v1
|
- uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 14
|
java-version: 16
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
path: ~/.m2/repository
|
path: ~/.m2/repository
|
||||||
@@ -25,12 +25,13 @@ jobs:
|
|||||||
- name: Build and Test
|
- name: Build and Test
|
||||||
run: mvn -B install --file main/pom.xml -Pcoverage
|
run: mvn -B install --file main/pom.xml -Pcoverage
|
||||||
- name: Run Codacy Coverage Reporter
|
- name: Run Codacy Coverage Reporter
|
||||||
|
if: github.repository == 'cryptomator/cryptomator'
|
||||||
run: |
|
run: |
|
||||||
curl -o ~/codacy-coverage-reporter.jar https://repo.maven.apache.org/maven2/com/codacy/codacy-coverage-reporter/7.1.0/codacy-coverage-reporter-7.1.0-assembly.jar
|
curl -o ~/codacy-coverage-reporter.jar https://repo.maven.apache.org/maven2/com/codacy/codacy-coverage-reporter/7.1.0/codacy-coverage-reporter-7.1.0-assembly.jar
|
||||||
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/commons/target/site/jacoco/jacoco.xml --partial
|
$JAVA_HOME/bin/java --illegal-access=permit -jar ~/codacy-coverage-reporter.jar report -l Java -r main/commons/target/site/jacoco/jacoco.xml --partial
|
||||||
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/ui/target/site/jacoco/jacoco.xml --partial
|
$JAVA_HOME/bin/java --illegal-access=permit -jar ~/codacy-coverage-reporter.jar report -l Java -r main/ui/target/site/jacoco/jacoco.xml --partial
|
||||||
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/launcher/target/site/jacoco/jacoco.xml --partial
|
$JAVA_HOME/bin/java --illegal-access=permit -jar ~/codacy-coverage-reporter.jar report -l Java -r main/launcher/target/site/jacoco/jacoco.xml --partial
|
||||||
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar final
|
$JAVA_HOME/bin/java --illegal-access=permit -jar ~/codacy-coverage-reporter.jar final
|
||||||
env:
|
env:
|
||||||
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||||
- name: Assemble buildkit-linux.zip
|
- name: Assemble buildkit-linux.zip
|
||||||
@@ -59,7 +60,7 @@ jobs:
|
|||||||
name: Draft a Release on GitHub Releases
|
name: Draft a Release on GitHub Releases
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'cryptomator/cryptomator'
|
||||||
steps:
|
steps:
|
||||||
- name: Download buildkit-linux.zip
|
- name: Download buildkit-linux.zip
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v1
|
||||||
@@ -86,6 +87,21 @@ jobs:
|
|||||||
release_name: ${{ github.ref }}
|
release_name: ${{ github.ref }}
|
||||||
body: |
|
body: |
|
||||||
:construction: Work in Progress
|
:construction: Work in Progress
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
* [ ] add Linux appimage, zsync file and signature file
|
||||||
|
* [ ] add Windows installer and signature file
|
||||||
|
* [ ] add MacOs disk image and signature file
|
||||||
|
|
||||||
|
## What's new
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
|
||||||
|
## Misc
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
:scroll: A complete list of closed issues is available [here](LINK)
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
- name: Upload buildkit-linux.zip to GitHub Releases
|
- name: Upload buildkit-linux.zip to GitHub Releases
|
||||||
|
|||||||
@@ -6,20 +6,32 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
closeTemplateViolation:
|
closeTemplateViolation:
|
||||||
name: Close bug reports that violate the issue template
|
name: Validate bug report against issue template
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: contains(github.event.issue.labels.*.name, 'type:bug')
|
||||||
steps:
|
steps:
|
||||||
- if: |
|
- name: Check "Description"
|
||||||
contains(github.event.issue.labels.*.name, 'type:bug')
|
if: |
|
||||||
&& (
|
!contains(github.event.issue.body, env.MUST_CONTAIN)
|
||||||
!contains(github.event.issue.body, '<!-- oooooo -->')
|
|| contains(toJson(github.event.issue.body), env.MUST_NOT_CONTAIN)
|
||||||
|| !contains(github.event.issue.body, '### Description')
|
run: exit 1
|
||||||
)
|
env:
|
||||||
name: Close Issue
|
MUST_CONTAIN: '### Description'
|
||||||
|
MUST_NOT_CONTAIN: '### Description\r\n\r\n[Summarize your problem.]\r\n\r\n### System Setup'
|
||||||
|
- name: Check "Steps to Reproduce"
|
||||||
|
if: |
|
||||||
|
!contains(github.event.issue.body, env.MUST_CONTAIN)
|
||||||
|
|| contains(toJson(github.event.issue.body), env.MUST_NOT_CONTAIN)
|
||||||
|
run: exit 1
|
||||||
|
env:
|
||||||
|
MUST_CONTAIN: '### Steps to Reproduce'
|
||||||
|
MUST_NOT_CONTAIN: '### Steps to Reproduce\r\n\r\n1. [First step]\r\n2. [Second step]\r\n3. [and so on…]\r\n\r\n#### Expected Behavior'
|
||||||
|
- name: Close issue if one of the checks failed
|
||||||
|
if: ${{ failure() }}
|
||||||
uses: peter-evans/close-issue@v1
|
uses: peter-evans/close-issue@v1
|
||||||
with:
|
with:
|
||||||
comment: |
|
comment: |
|
||||||
This bug report did ignore our issue template. 😞
|
This bug report did ignore our issue template. 😞
|
||||||
Auto-closing this issue, since it is most likely not useful.
|
Auto-closing this issue, since it is most likely not useful.
|
||||||
|
|
||||||
_This decision was made by a bot. If you think the bot is wrong, let us know and we'll reopen this issue._
|
_This decision was made by a bot. If you think the bot is wrong, let us know and we'll reopen this issue._
|
||||||
|
|||||||
@@ -21,5 +21,6 @@ pom.xml.versionsBackup
|
|||||||
.idea/compiler.xml
|
.idea/compiler.xml
|
||||||
.idea/encodings.xml
|
.idea/encodings.xml
|
||||||
.idea/jarRepositories.xml
|
.idea/jarRepositories.xml
|
||||||
|
.idea/uiDesigner.xml
|
||||||
.idea/**/libraries/
|
.idea/**/libraries/
|
||||||
*.iml
|
*.iml
|
||||||
Generated
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_14" default="false" project-jdk-name="14" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_16" project-jdk-name="16" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
|
<configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="launcher" />
|
<module name="launcher" />
|
||||||
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath="~/.config/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/.config/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/.local/share/Cryptomator/logs" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator/mnt" -Xss20m -Xmx512m" />
|
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath="~/.config/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/.config/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/.local/share/Cryptomator/logs" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator/mnt" -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
|
<configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="launcher" />
|
<module name="launcher" />
|
||||||
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath="~/.config/Cryptomator-Dev/settings.json" -Dcryptomator.ipcPortPath="~/.config/Cryptomator-Dev/ipcPort.bin" -Dcryptomator.logDir="~/.local/share/Cryptomator-Dev/logs" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator-Dev/mnt" -Dfuse.experimental="true" -Xss20m -Xmx512m" />
|
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath="~/.config/Cryptomator-Dev/settings.json" -Dcryptomator.ipcPortPath="~/.config/Cryptomator-Dev/ipcPort.bin" -Dcryptomator.logDir="~/.local/share/Cryptomator-Dev/logs" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator-Dev/mnt" -Dcryptomator.showTrayIcon=true -Dfuse.experimental="true" -Xss20m -Xmx512m" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
|
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="launcher" />
|
<module name="launcher" />
|
||||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" -Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" -Dcryptomator.mountPointsDir="~/Cryptomator" -Xss2m -Xmx512m" />
|
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" -Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" -Dcryptomator.mountPointsDir="~/Cryptomator" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
|
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="launcher" />
|
<module name="launcher" />
|
||||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator-Dev/settings.json" -Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator-Dev/ipcPort.bin" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator-Dev" -Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator-Dev/keychain.json" -Dcryptomator.mountPointsDir="~/Cryptomator-Dev" -Dfuse.experimental="true" -Xss2m -Xmx512m" />
|
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator-Dev/settings.json" -Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator-Dev/ipcPort.bin" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator-Dev" -Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator-Dev/keychain.json" -Dcryptomator.mountPointsDir="~/Cryptomator-Dev" -Dfuse.experimental="true" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
</envs>
|
</envs>
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="launcher" />
|
<module name="launcher" />
|
||||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/Library/Application Support/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/Library/Logs/Cryptomator" -Xss2m -Xmx512m -ea" />
|
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/Library/Application Support/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/Library/Logs/Cryptomator" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
</envs>
|
</envs>
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="launcher" />
|
<module name="launcher" />
|
||||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator-Dev/settings.json" -Dcryptomator.ipcPortPath="~/Library/Application Support/Cryptomator-Dev/ipcPort.bin" -Dcryptomator.logDir="~/Library/Logs/Cryptomator-Dev" -Xss2m -Xmx512m -ea" />
|
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator-Dev/settings.json" -Dcryptomator.ipcPortPath="~/Library/Application Support/Cryptomator-Dev/ipcPort.bin" -Dcryptomator.logDir="~/Library/Logs/Cryptomator-Dev" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://github.com/cryptomator/cryptomator/actions?query=workflow%3ABuild)
|
[](https://github.com/cryptomator/cryptomator/actions?query=workflow%3ABuild)
|
||||||
[](https://snyk.io/test/github/cryptomator/cryptomator?targetFile=main%2Fpom.xml)
|
[](https://snyk.io/test/github/cryptomator/cryptomator?targetFile=main%2Fpom.xml)
|
||||||
[](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptomator&utm_campaign=Badge_Grade)
|
[](https://www.codacy.com/gh/cryptomator/cryptomator/dashboard)
|
||||||
[](http://twitter.com/Cryptomator)
|
[](http://twitter.com/Cryptomator)
|
||||||
[](https://translate.cryptomator.org/)
|
[](https://translate.cryptomator.org/)
|
||||||
[](https://github.com/cryptomator/cryptomator/releases/latest)
|
[](https://github.com/cryptomator/cryptomator/releases/latest)
|
||||||
@@ -17,11 +17,26 @@ Cryptomator is provided free of charge as an open-source project despite the hig
|
|||||||
|
|
||||||
### Gold Sponsors
|
### Gold Sponsors
|
||||||
|
|
||||||
[<img src="https://cryptomator.org/img/sponsors/geewhiz.svg" alt="gee-whiz" height="96">](https://www.gee-whiz.de/)
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://www.gee-whiz.de/"><img src="https://cryptomator.org/img/sponsors/geewhiz.svg" alt="gee-whiz" height="80"></a></td>
|
||||||
|
<td><a href="https://proxy-hub.com/"><img src="https://cryptomator.org/img/sponsors/proxyhub.svg" alt="Proxy-Hub" height="80"></a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
### Silver Sponsors
|
### Silver Sponsors
|
||||||
|
|
||||||
[](https://thebestvpn.com/)
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://thebestvpn.com/"><img src="https://cryptomator.org/img/sponsors/thebestvpn@2x.png" alt="TheBestVPN" height="64"></a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
- [Jameson Lopp](https://www.lopp.net/)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -33,7 +48,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Works with Dropbox, Google Drive, OneDrive, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory
|
- Works with Dropbox, Google Drive, OneDrive, MEGA, pCloud, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory
|
||||||
- Open Source means: No backdoors, control is better than trust
|
- Open Source means: No backdoors, control is better than trust
|
||||||
- Client-side: No accounts, no data shared with any online service
|
- Client-side: No accounts, no data shared with any online service
|
||||||
- Totally transparent: Just work on the virtual drive as if it were a USB flash drive
|
- Totally transparent: Just work on the virtual drive as if it were a USB flash drive
|
||||||
@@ -65,7 +80,7 @@ For more information on the security details visit [cryptomator.org](https://doc
|
|||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
* JDK 14 (e.g. adoptopenjdk)
|
* JDK 16 (e.g. adoptopenjdk)
|
||||||
* Maven 3
|
* Maven 3
|
||||||
* Optional: OS-dependent build tools for native packaging (see [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Linux](https://github.com/cryptomator/builder-containers))
|
* Optional: OS-dependent build tools for native packaging (see [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Linux](https://github.com/cryptomator/builder-containers))
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.5.10</version>
|
<version>1.6.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>buildkit</artifactId>
|
<artifactId>buildkit</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.5.10</version>
|
<version>1.6.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commons</artifactId>
|
<artifactId>commons</artifactId>
|
||||||
<name>Cryptomator Commons</name>
|
<name>Cryptomator Commons</name>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.cryptomator.common.settings.SettingsProvider;
|
|||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.common.vaults.VaultComponent;
|
import org.cryptomator.common.vaults.VaultComponent;
|
||||||
import org.cryptomator.common.vaults.VaultListManager;
|
import org.cryptomator.common.vaults.VaultListManager;
|
||||||
|
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -25,6 +26,8 @@ import javafx.beans.binding.Binding;
|
|||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@@ -47,10 +50,28 @@ public abstract class CommonsModule {
|
|||||||
@Named("licensePublicKey")
|
@Named("licensePublicKey")
|
||||||
static String provideLicensePublicKey() {
|
static String provideLicensePublicKey() {
|
||||||
// in PEM format without the dash-escaped begin/end lines
|
// in PEM format without the dash-escaped begin/end lines
|
||||||
return "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7NfnqiZbg2KTmoflmZ71PbXru7oW" //
|
return """
|
||||||
+ "fmnV2yv3eDjlDfGruBrqz9TtXBZV/eYWt31xu1osIqaT12lKBvZ511aaAkIBeOEV" //
|
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7NfnqiZbg2KTmoflmZ71PbXru7oW\
|
||||||
+ "gwcBIlJr6kUw7NKzeJt7r2rrsOyQoOG2nWc/Of/NBqA3mIZRHk5Aq1YupFdD26QE" //
|
fmnV2yv3eDjlDfGruBrqz9TtXBZV/eYWt31xu1osIqaT12lKBvZ511aaAkIBeOEV\
|
||||||
+ "r0DzRyj4ixPIt38CQB8=";
|
gwcBIlJr6kUw7NKzeJt7r2rrsOyQoOG2nWc/Of/NBqA3mIZRHk5Aq1YupFdD26QE\
|
||||||
|
r0DzRyj4ixPIt38CQB8=\
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
static SecureRandom provideCSPRNG() {
|
||||||
|
try {
|
||||||
|
return SecureRandom.getInstanceStrong();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
static MasterkeyFileAccess provideMasterkeyFileAccess(SecureRandom csprng) {
|
||||||
|
return new MasterkeyFileAccess(Constants.PEPPER, csprng);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -3,5 +3,8 @@ package org.cryptomator.common;
|
|||||||
public interface Constants {
|
public interface Constants {
|
||||||
|
|
||||||
String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
||||||
|
String MASTERKEY_BACKUP_SUFFIX = ".bkup";
|
||||||
|
String VAULTCONFIG_FILENAME = "vault.cryptomator";
|
||||||
|
byte[] PEPPER = new byte[0];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,13 @@ import java.util.stream.StreamSupport;
|
|||||||
public class Environment {
|
public class Environment {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(Environment.class);
|
private static final Logger LOG = LoggerFactory.getLogger(Environment.class);
|
||||||
private static final String USER_HOME = System.getProperty("user.home");
|
|
||||||
private static final Path RELATIVE_HOME_DIR = Paths.get("~");
|
private static final Path RELATIVE_HOME_DIR = Paths.get("~");
|
||||||
private static final Path ABSOLUTE_HOME_DIR = Paths.get(USER_HOME);
|
|
||||||
private static final char PATH_LIST_SEP = ':';
|
private static final char PATH_LIST_SEP = ':';
|
||||||
private static final int DEFAULT_MIN_PW_LENGTH = 8;
|
private static final int DEFAULT_MIN_PW_LENGTH = 8;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public Environment() {
|
public Environment() {
|
||||||
|
LOG.debug("user.home: {}", System.getProperty("user.home"));
|
||||||
LOG.debug("java.library.path: {}", System.getProperty("java.library.path"));
|
LOG.debug("java.library.path: {}", System.getProperty("java.library.path"));
|
||||||
LOG.debug("user.language: {}", System.getProperty("user.language"));
|
LOG.debug("user.language: {}", System.getProperty("user.language"));
|
||||||
LOG.debug("user.region: {}", System.getProperty("user.region"));
|
LOG.debug("user.region: {}", System.getProperty("user.region"));
|
||||||
@@ -40,6 +39,7 @@ public class Environment {
|
|||||||
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
|
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
|
||||||
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
|
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
|
||||||
LOG.debug("cryptomator.buildNumber: {}", System.getProperty("cryptomator.buildNumber"));
|
LOG.debug("cryptomator.buildNumber: {}", System.getProperty("cryptomator.buildNumber"));
|
||||||
|
LOG.debug("cryptomator.showTrayIcon: {}", System.getProperty("cryptomator.showTrayIcon"));
|
||||||
LOG.debug("fuse.experimental: {}", Boolean.getBoolean("fuse.experimental"));
|
LOG.debug("fuse.experimental: {}", Boolean.getBoolean("fuse.experimental"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +75,11 @@ public class Environment {
|
|||||||
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
|
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean showTrayIcon() {
|
||||||
|
return Boolean.getBoolean("cryptomator.showTrayIcon");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated // TODO: remove as soon as custom mount path works properly on Win+Fuse
|
||||||
public boolean useExperimentalFuse() {
|
public boolean useExperimentalFuse() {
|
||||||
return Boolean.getBoolean("fuse.experimental");
|
return Boolean.getBoolean("fuse.experimental");
|
||||||
}
|
}
|
||||||
@@ -92,8 +97,13 @@ public class Environment {
|
|||||||
String value = System.getProperty(propertyName);
|
String value = System.getProperty(propertyName);
|
||||||
return Optional.ofNullable(value).map(Paths::get);
|
return Optional.ofNullable(value).map(Paths::get);
|
||||||
}
|
}
|
||||||
// visible for testing
|
|
||||||
|
|
||||||
|
// visible for testing
|
||||||
|
Path getHomeDir() {
|
||||||
|
return getPath("user.home").orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// visible for testing
|
||||||
Stream<Path> getPaths(String propertyName) {
|
Stream<Path> getPaths(String propertyName) {
|
||||||
Stream<String> rawSettingsPaths = getRawList(propertyName, PATH_LIST_SEP);
|
Stream<String> rawSettingsPaths = getRawList(propertyName, PATH_LIST_SEP);
|
||||||
return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Paths::get).map(this::replaceHomeDir);
|
return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Paths::get).map(this::replaceHomeDir);
|
||||||
@@ -101,7 +111,7 @@ public class Environment {
|
|||||||
|
|
||||||
private Path replaceHomeDir(Path path) {
|
private Path replaceHomeDir(Path path) {
|
||||||
if (path.startsWith(RELATIVE_HOME_DIR)) {
|
if (path.startsWith(RELATIVE_HOME_DIR)) {
|
||||||
return ABSOLUTE_HOME_DIR.resolve(RELATIVE_HOME_DIR.relativize(path));
|
return getHomeDir().resolve(RELATIVE_HOME_DIR.relativize(path));
|
||||||
} else {
|
} else {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.common;
|
|
||||||
|
|
||||||
import com.google.common.base.Throwables;
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.function.UnaryOperator;
|
|
||||||
|
|
||||||
public final class LazyInitializer {
|
|
||||||
|
|
||||||
private LazyInitializer() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as {@link #initializeLazily(AtomicReference, SupplierThrowingException, Class)} except that no checked exception may be thrown by the factory function.
|
|
||||||
*
|
|
||||||
* @param <T> Type of the value
|
|
||||||
* @param reference A reference to a maybe not yet initialized value.
|
|
||||||
* @param factory A factory providing a value for the reference, if it doesn't exist yet. The factory may be invoked multiple times, but only one result will survive.
|
|
||||||
* @return The initialized value
|
|
||||||
*/
|
|
||||||
public static <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
|
|
||||||
SupplierThrowingException<T, RuntimeException> factoryThrowingRuntimeExceptions = () -> factory.get();
|
|
||||||
return initializeLazily(reference, factoryThrowingRuntimeExceptions, RuntimeException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Threadsafe lazy initialization pattern as proposed on http://stackoverflow.com/a/30247202/4014509
|
|
||||||
*
|
|
||||||
* @param <T> Type of the value
|
|
||||||
* @param <E> Type of the any expected exception that may occur during initialization
|
|
||||||
* @param reference A reference to a maybe not yet initialized value.
|
|
||||||
* @param factory A factory providing a value for the reference, if it doesn't exist yet. The factory may be invoked multiple times, but only one result will survive.
|
|
||||||
* @param exceptionType Expected exception type.
|
|
||||||
* @return The initialized value
|
|
||||||
* @throws E Exception thrown by the factory function.
|
|
||||||
*/
|
|
||||||
public static <T, E extends Exception> T initializeLazily(AtomicReference<T> reference, SupplierThrowingException<T, E> factory, Class<E> exceptionType) throws E {
|
|
||||||
final T existing = reference.get();
|
|
||||||
if (existing != null) {
|
|
||||||
return existing;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return reference.updateAndGet(invokeFactoryIfNull(factory));
|
|
||||||
} catch (InitializationException e) {
|
|
||||||
Throwables.throwIfUnchecked(e.getCause());
|
|
||||||
Throwables.throwIfInstanceOf(e.getCause(), exceptionType);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T, E extends Exception> UnaryOperator<T> invokeFactoryIfNull(SupplierThrowingException<T, E> factory) throws InitializationException {
|
|
||||||
return currentValue -> {
|
|
||||||
if (currentValue == null) {
|
|
||||||
try {
|
|
||||||
return factory.get();
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new InitializationException(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return currentValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class InitializationException extends RuntimeException {
|
|
||||||
|
|
||||||
public InitializationException(Throwable cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,8 +33,8 @@ class LicenseChecker {
|
|||||||
try {
|
try {
|
||||||
byte[] keyBytes = BaseEncoding.base64().decode(pemEncodedPublicKey);
|
byte[] keyBytes = BaseEncoding.base64().decode(pemEncodedPublicKey);
|
||||||
PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes));
|
PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes));
|
||||||
if (key instanceof ECPublicKey) {
|
if (key instanceof ECPublicKey k) {
|
||||||
return (ECPublicKey) key;
|
return k;
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("Key not an EC public key.");
|
throw new IllegalStateException("Key not an EC public key.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ public class KeychainManager implements KeychainAccessProvider {
|
|||||||
return keychain.getValue() != null;
|
return keychain.getValue() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLocked() {
|
||||||
|
return keychain.getValue() == null || keychain.get().isLocked();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the keychain knows a passphrase for the given key.
|
* Checks if the keychain knows a passphrase for the given key.
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
+4
-26
@@ -3,25 +3,22 @@ package org.cryptomator.common.mountpoint;
|
|||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
import org.cryptomator.common.vaults.Volume;
|
import org.cryptomator.common.vaults.Volume;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
class MacVolumeMountChooser implements MountPointChooser {
|
class MacVolumeMountChooser implements MountPointChooser {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(MacVolumeMountChooser.class);
|
|
||||||
private static final int MAX_MOUNTPOINT_CREATION_RETRIES = 10;
|
|
||||||
private static final Path VOLUME_PATH = Path.of("/Volumes");
|
private static final Path VOLUME_PATH = Path.of("/Volumes");
|
||||||
|
|
||||||
private final VaultSettings vaultSettings;
|
private final VaultSettings vaultSettings;
|
||||||
|
private final MountPointHelper helper;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public MacVolumeMountChooser(VaultSettings vaultSettings) {
|
public MacVolumeMountChooser(VaultSettings vaultSettings, MountPointHelper helper) {
|
||||||
this.vaultSettings = vaultSettings;
|
this.vaultSettings = vaultSettings;
|
||||||
|
this.helper = helper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -31,26 +28,7 @@ class MacVolumeMountChooser implements MountPointChooser {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||||
String basename = this.vaultSettings.mountName().get();
|
return Optional.of(helper.chooseTemporaryMountPoint(vaultSettings, VOLUME_PATH));
|
||||||
// regular
|
|
||||||
Path mountPoint = VOLUME_PATH.resolve(basename);
|
|
||||||
if (Files.notExists(mountPoint)) {
|
|
||||||
return Optional.of(mountPoint);
|
|
||||||
}
|
|
||||||
// with id
|
|
||||||
mountPoint = VOLUME_PATH.resolve(basename + " (" + vaultSettings.getId() + ")");
|
|
||||||
if (Files.notExists(mountPoint)) {
|
|
||||||
return Optional.of(mountPoint);
|
|
||||||
}
|
|
||||||
// with id and count
|
|
||||||
for (int i = 1; i < MAX_MOUNTPOINT_CREATION_RETRIES; i++) {
|
|
||||||
mountPoint = VOLUME_PATH.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
|
|
||||||
if (Files.notExists(mountPoint)) {
|
|
||||||
return Optional.of(mountPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOG.error("Failed to find feasible mountpoint at /Volumes/{}_x. Giving up after {} attempts.", basename, MAX_MOUNTPOINT_CREATION_RETRIES);
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+39
-8
@@ -1,11 +1,13 @@
|
|||||||
package org.cryptomator.common.mountpoint;
|
package org.cryptomator.common.mountpoint;
|
||||||
|
|
||||||
import org.cryptomator.common.Environment;
|
import org.cryptomator.common.Environment;
|
||||||
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.DirectoryNotEmptyException;
|
import java.nio.file.DirectoryNotEmptyException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -15,27 +17,50 @@ import java.nio.file.attribute.BasicFileAttributes;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class IrregularUnmountCleaner {
|
class MountPointHelper {
|
||||||
|
|
||||||
public static Logger LOG = LoggerFactory.getLogger(IrregularUnmountCleaner.class);
|
public static Logger LOG = LoggerFactory.getLogger(MountPointHelper.class);
|
||||||
|
private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
|
||||||
|
|
||||||
private final Optional<Path> tmpMountPointDir;
|
private final Optional<Path> tmpMountPointDir;
|
||||||
private volatile boolean alreadyChecked = false;
|
private volatile boolean unmountDebrisCleared = false;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public IrregularUnmountCleaner(Environment env) {
|
public MountPointHelper(Environment env) {
|
||||||
this.tmpMountPointDir = env.getMountPointsDir();
|
this.tmpMountPointDir = env.getMountPointsDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Path chooseTemporaryMountPoint(VaultSettings vaultSettings, Path parentDir) {
|
||||||
|
String basename = vaultSettings.mountName().get();
|
||||||
|
//regular
|
||||||
|
Path mountPoint = parentDir.resolve(basename);
|
||||||
|
if (Files.notExists(mountPoint)) {
|
||||||
|
return mountPoint;
|
||||||
|
}
|
||||||
|
//with id
|
||||||
|
mountPoint = parentDir.resolve(basename + " (" + vaultSettings.getId() + ")");
|
||||||
|
if (Files.notExists(mountPoint)) {
|
||||||
|
return mountPoint;
|
||||||
|
}
|
||||||
|
//with id and count
|
||||||
|
for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
|
||||||
|
mountPoint = parentDir.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
|
||||||
|
if (Files.notExists(mountPoint)) {
|
||||||
|
return mountPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parentDir, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized void clearIrregularUnmountDebrisIfNeeded() {
|
public synchronized void clearIrregularUnmountDebrisIfNeeded() {
|
||||||
if (alreadyChecked || tmpMountPointDir.isEmpty()) {
|
if (unmountDebrisCleared || tmpMountPointDir.isEmpty()) {
|
||||||
return; //nuthin to do
|
return; // nothing to do
|
||||||
}
|
}
|
||||||
if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) {
|
if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) {
|
||||||
clearIrregularUnmountDebris(tmpMountPointDir.get());
|
clearIrregularUnmountDebris(tmpMountPointDir.get());
|
||||||
}
|
}
|
||||||
alreadyChecked = true;
|
unmountDebrisCleared = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {
|
private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {
|
||||||
@@ -66,13 +91,14 @@ class IrregularUnmountCleaner {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e);
|
LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e);
|
||||||
} finally {
|
} finally {
|
||||||
alreadyChecked = true;
|
unmountDebrisCleared = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteEmptyDir(Path dir) throws IOException {
|
private void deleteEmptyDir(Path dir) throws IOException {
|
||||||
assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
|
assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
|
||||||
try {
|
try {
|
||||||
|
ensureIsEmpty(dir);
|
||||||
Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents)
|
Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents)
|
||||||
} catch (DirectoryNotEmptyException e) {
|
} catch (DirectoryNotEmptyException e) {
|
||||||
LOG.info("Found non-empty directory in mountpoint dir: {}", dir);
|
LOG.info("Found non-empty directory in mountpoint dir: {}", dir);
|
||||||
@@ -86,4 +112,9 @@ class IrregularUnmountCleaner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureIsEmpty(Path dir) throws IOException {
|
||||||
|
if (Files.newDirectoryStream(dir).iterator().hasNext()) {
|
||||||
|
throw new DirectoryNotEmptyException(dir.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+5
-32
@@ -7,7 +7,6 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -16,18 +15,16 @@ import java.util.Optional;
|
|||||||
class TemporaryMountPointChooser implements MountPointChooser {
|
class TemporaryMountPointChooser implements MountPointChooser {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
|
private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
|
||||||
private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
|
|
||||||
|
|
||||||
private final VaultSettings vaultSettings;
|
private final VaultSettings vaultSettings;
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
private final IrregularUnmountCleaner cleaner;
|
private final MountPointHelper helper;
|
||||||
private volatile boolean clearedDebris;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, IrregularUnmountCleaner cleaner) {
|
public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, MountPointHelper helper) {
|
||||||
this.vaultSettings = vaultSettings;
|
this.vaultSettings = vaultSettings;
|
||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
this.cleaner = cleaner;
|
this.helper = helper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -44,32 +41,8 @@ class TemporaryMountPointChooser implements MountPointChooser {
|
|||||||
assert environment.getMountPointsDir().isPresent();
|
assert environment.getMountPointsDir().isPresent();
|
||||||
//clean leftovers of not-regularly unmounted vaults
|
//clean leftovers of not-regularly unmounted vaults
|
||||||
//see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061
|
//see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061
|
||||||
cleaner.clearIrregularUnmountDebrisIfNeeded();
|
helper.clearIrregularUnmountDebrisIfNeeded();
|
||||||
return this.environment.getMountPointsDir().map(this::choose);
|
return this.environment.getMountPointsDir().map(dir -> this.helper.chooseTemporaryMountPoint(this.vaultSettings, dir));
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Path choose(Path parent) {
|
|
||||||
String basename = this.vaultSettings.mountName().get();
|
|
||||||
//regular
|
|
||||||
Path mountPoint = parent.resolve(basename);
|
|
||||||
if (Files.notExists(mountPoint)) {
|
|
||||||
return mountPoint;
|
|
||||||
}
|
|
||||||
//with id
|
|
||||||
mountPoint = parent.resolve(basename + " (" + vaultSettings.getId() + ")");
|
|
||||||
if (Files.notExists(mountPoint)) {
|
|
||||||
return mountPoint;
|
|
||||||
}
|
|
||||||
//with id and count
|
|
||||||
for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
|
|
||||||
mountPoint = parent.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
|
|
||||||
if (Files.notExists(mountPoint)) {
|
|
||||||
return mountPoint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parent, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
package org.cryptomator.common.settings;
|
package org.cryptomator.common.settings;
|
||||||
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
|
import org.cryptomator.common.Environment;
|
||||||
|
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
@@ -39,7 +40,8 @@ public class Settings {
|
|||||||
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
|
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
|
||||||
public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = SystemUtils.IS_OS_WINDOWS ? KeychainBackend.WIN_SYSTEM_KEYCHAIN : SystemUtils.IS_OS_MAC ? KeychainBackend.MAC_SYSTEM_KEYCHAIN : KeychainBackend.GNOME;
|
public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = SystemUtils.IS_OS_WINDOWS ? KeychainBackend.WIN_SYSTEM_KEYCHAIN : SystemUtils.IS_OS_MAC ? KeychainBackend.MAC_SYSTEM_KEYCHAIN : KeychainBackend.GNOME;
|
||||||
public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
|
public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
|
||||||
private static final String DEFAULT_LICENSE_KEY = "";
|
public static final String DEFAULT_LICENSE_KEY = "";
|
||||||
|
public static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false;
|
||||||
|
|
||||||
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
|
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
|
||||||
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
|
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
|
||||||
@@ -54,13 +56,17 @@ public class Settings {
|
|||||||
private final ObjectProperty<KeychainBackend> keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND);
|
private final ObjectProperty<KeychainBackend> keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND);
|
||||||
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
|
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
|
||||||
private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
|
private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
|
||||||
|
private final BooleanProperty showMinimizeButton = new SimpleBooleanProperty(DEFAULT_SHOW_MINIMIZE_BUTTON);
|
||||||
|
private final BooleanProperty showTrayIcon;
|
||||||
|
|
||||||
private Consumer<Settings> saveCmd;
|
private Consumer<Settings> saveCmd;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Package-private constructor; use {@link SettingsProvider}.
|
* Package-private constructor; use {@link SettingsProvider}.
|
||||||
*/
|
*/
|
||||||
Settings() {
|
Settings(Environment env) {
|
||||||
|
this.showTrayIcon = new SimpleBooleanProperty(env.showTrayIcon());
|
||||||
|
|
||||||
directories.addListener(this::somethingChanged);
|
directories.addListener(this::somethingChanged);
|
||||||
askedForUpdateCheck.addListener(this::somethingChanged);
|
askedForUpdateCheck.addListener(this::somethingChanged);
|
||||||
checkForUpdates.addListener(this::somethingChanged);
|
checkForUpdates.addListener(this::somethingChanged);
|
||||||
@@ -74,6 +80,8 @@ public class Settings {
|
|||||||
keychainBackend.addListener(this::somethingChanged);
|
keychainBackend.addListener(this::somethingChanged);
|
||||||
userInterfaceOrientation.addListener(this::somethingChanged);
|
userInterfaceOrientation.addListener(this::somethingChanged);
|
||||||
licenseKey.addListener(this::somethingChanged);
|
licenseKey.addListener(this::somethingChanged);
|
||||||
|
showMinimizeButton.addListener(this::somethingChanged);
|
||||||
|
showTrayIcon.addListener(this::somethingChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSaveCmd(Consumer<Settings> saveCmd) {
|
void setSaveCmd(Consumer<Settings> saveCmd) {
|
||||||
@@ -141,4 +149,12 @@ public class Settings {
|
|||||||
public StringProperty licenseKey() {
|
public StringProperty licenseKey() {
|
||||||
return licenseKey;
|
return licenseKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BooleanProperty showMinimizeButton() {
|
||||||
|
return showMinimizeButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty showTrayIcon() {
|
||||||
|
return showTrayIcon;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-2
@@ -9,19 +9,29 @@ import com.google.gson.TypeAdapter;
|
|||||||
import com.google.gson.stream.JsonReader;
|
import com.google.gson.stream.JsonReader;
|
||||||
import com.google.gson.stream.JsonToken;
|
import com.google.gson.stream.JsonToken;
|
||||||
import com.google.gson.stream.JsonWriter;
|
import com.google.gson.stream.JsonWriter;
|
||||||
|
import org.cryptomator.common.Environment;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
import javafx.geometry.NodeOrientation;
|
import javafx.geometry.NodeOrientation;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class);
|
private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class);
|
||||||
|
|
||||||
private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter();
|
private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter();
|
||||||
|
private final Environment env;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public SettingsJsonAdapter(Environment env) {
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(JsonWriter out, Settings value) throws IOException {
|
public void write(JsonWriter out, Settings value) throws IOException {
|
||||||
@@ -40,6 +50,8 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
|
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
|
||||||
out.name("keychainBackend").value(value.keychainBackend().get().name());
|
out.name("keychainBackend").value(value.keychainBackend().get().name());
|
||||||
out.name("licenseKey").value(value.licenseKey().get());
|
out.name("licenseKey").value(value.licenseKey().get());
|
||||||
|
out.name("showMinimizeButton").value(value.showMinimizeButton().get());
|
||||||
|
out.name("showTrayIcon").value(value.showTrayIcon().get());
|
||||||
out.endObject();
|
out.endObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +65,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Settings read(JsonReader in) throws IOException {
|
public Settings read(JsonReader in) throws IOException {
|
||||||
Settings settings = new Settings();
|
Settings settings = new Settings(env);
|
||||||
|
|
||||||
in.beginObject();
|
in.beginObject();
|
||||||
while (in.hasNext()) {
|
while (in.hasNext()) {
|
||||||
@@ -72,6 +84,8 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
|
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
|
||||||
case "keychainBackend" -> settings.keychainBackend().set(parseKeychainBackend(in.nextString()));
|
case "keychainBackend" -> settings.keychainBackend().set(parseKeychainBackend(in.nextString()));
|
||||||
case "licenseKey" -> settings.licenseKey().set(in.nextString());
|
case "licenseKey" -> settings.licenseKey().set(in.nextString());
|
||||||
|
case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean());
|
||||||
|
case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean());
|
||||||
default -> {
|
default -> {
|
||||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||||
in.skipValue();
|
in.skipValue();
|
||||||
@@ -137,5 +151,4 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
in.endArray();
|
in.endArray();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -8,13 +8,13 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.common.settings;
|
package org.cryptomator.common.settings;
|
||||||
|
|
||||||
|
import com.google.common.base.Suppliers;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
import org.cryptomator.common.Environment;
|
import org.cryptomator.common.Environment;
|
||||||
import org.cryptomator.common.LazyInitializer;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -48,14 +48,15 @@ public class SettingsProvider implements Supplier<Settings> {
|
|||||||
private static final long SAVE_DELAY_MS = 1000;
|
private static final long SAVE_DELAY_MS = 1000;
|
||||||
|
|
||||||
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
|
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
|
||||||
private final AtomicReference<Settings> settings = new AtomicReference<>();
|
private final Supplier<Settings> settings = Suppliers.memoize(this::load);
|
||||||
private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter();
|
private final SettingsJsonAdapter settingsJsonAdapter;
|
||||||
private final Environment env;
|
private final Environment env;
|
||||||
private final ScheduledExecutorService scheduler;
|
private final ScheduledExecutorService scheduler;
|
||||||
private final Gson gson;
|
private final Gson gson;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public SettingsProvider(Environment env, ScheduledExecutorService scheduler) {
|
public SettingsProvider(SettingsJsonAdapter settingsJsonAdapter, Environment env, ScheduledExecutorService scheduler) {
|
||||||
|
this.settingsJsonAdapter = settingsJsonAdapter;
|
||||||
this.env = env;
|
this.env = env;
|
||||||
this.scheduler = scheduler;
|
this.scheduler = scheduler;
|
||||||
this.gson = new GsonBuilder() //
|
this.gson = new GsonBuilder() //
|
||||||
@@ -66,11 +67,11 @@ public class SettingsProvider implements Supplier<Settings> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Settings get() {
|
public Settings get() {
|
||||||
return LazyInitializer.initializeLazily(settings, this::load);
|
return settings.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Settings load() {
|
private Settings load() {
|
||||||
Settings settings = env.getSettingsPath().flatMap(this::tryLoad).findFirst().orElse(new Settings());
|
Settings settings = env.getSettingsPath().flatMap(this::tryLoad).findFirst().orElse(new Settings(env));
|
||||||
settings.setSaveCmd(this::scheduleSave);
|
settings.setSaveCmd(this::scheduleSave);
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
@@ -90,7 +91,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
|||||||
}
|
}
|
||||||
} catch (NoSuchFileException e) {
|
} catch (NoSuchFileException e) {
|
||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
} catch (IOException e) {
|
} catch (IOException | JsonParseException e) {
|
||||||
LOG.warn("Exception while loading settings from " + path, e);
|
LOG.warn("Exception while loading settings from " + path, e);
|
||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public enum UiTheme {
|
|||||||
AUTOMATIC("preferences.general.theme.automatic");
|
AUTOMATIC("preferences.general.theme.automatic");
|
||||||
|
|
||||||
public static UiTheme[] applicableValues() {
|
public static UiTheme[] applicableValues() {
|
||||||
if (SystemUtils.IS_OS_MAC) {
|
if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS) {
|
||||||
return values();
|
return values();
|
||||||
} else {
|
} else {
|
||||||
return new UiTheme[]{LIGHT, DARK};
|
return new UiTheme[]{LIGHT, DARK};
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ import java.nio.file.Path;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The settings specific to a single vault.
|
* The settings specific to a single vault.
|
||||||
@@ -37,7 +35,7 @@ public class VaultSettings {
|
|||||||
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
|
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
|
||||||
public static final boolean DEFAULT_USES_READONLY_MODE = false;
|
public static final boolean DEFAULT_USES_READONLY_MODE = false;
|
||||||
public static final String DEFAULT_MOUNT_FLAGS = "";
|
public static final String DEFAULT_MOUNT_FLAGS = "";
|
||||||
public static final int DEFAULT_FILENAME_LENGTH_LIMIT = -1;
|
public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
|
||||||
public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
|
public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
|
||||||
|
|
||||||
private static final Random RNG = new Random();
|
private static final Random RNG = new Random();
|
||||||
@@ -52,7 +50,7 @@ public class VaultSettings {
|
|||||||
private final StringProperty customMountPath = new SimpleStringProperty();
|
private final StringProperty customMountPath = new SimpleStringProperty();
|
||||||
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
|
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
|
||||||
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
|
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
|
||||||
private final IntegerProperty filenameLengthLimit = new SimpleIntegerProperty(DEFAULT_FILENAME_LENGTH_LIMIT);
|
private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
|
||||||
private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
|
private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
|
||||||
|
|
||||||
private final StringBinding mountName;
|
private final StringBinding mountName;
|
||||||
@@ -63,7 +61,7 @@ public class VaultSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Observable[] observables() {
|
Observable[] observables() {
|
||||||
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit, actionAfterUnlock};
|
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static VaultSettings withRandomId() {
|
public static VaultSettings withRandomId() {
|
||||||
@@ -152,8 +150,8 @@ public class VaultSettings {
|
|||||||
return mountFlags;
|
return mountFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IntegerProperty filenameLengthLimit() {
|
public IntegerProperty maxCleartextFilenameLength() {
|
||||||
return filenameLengthLimit;
|
return maxCleartextFilenameLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
|
public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
|
||||||
@@ -173,8 +171,7 @@ public class VaultSettings {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
if (obj instanceof VaultSettings && obj.getClass().equals(this.getClass())) {
|
if (obj instanceof VaultSettings other && obj.getClass().equals(this.getClass())) {
|
||||||
VaultSettings other = (VaultSettings) obj;
|
|
||||||
return Objects.equals(this.id, other.id);
|
return Objects.equals(this.id, other.id);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+4
-4
@@ -29,7 +29,7 @@ class VaultSettingsJsonAdapter {
|
|||||||
out.name("customMountPath").value(value.customMountPath().get());
|
out.name("customMountPath").value(value.customMountPath().get());
|
||||||
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
|
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
|
||||||
out.name("mountFlags").value(value.mountFlags().get());
|
out.name("mountFlags").value(value.mountFlags().get());
|
||||||
out.name("filenameLengthLimit").value(value.filenameLengthLimit().get());
|
out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get());
|
||||||
out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
|
out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
|
||||||
out.endObject();
|
out.endObject();
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ class VaultSettingsJsonAdapter {
|
|||||||
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
|
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
|
||||||
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
|
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
|
||||||
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
|
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
|
||||||
int filenameLengthLimit = VaultSettings.DEFAULT_FILENAME_LENGTH_LIMIT;
|
int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
|
||||||
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
|
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
|
||||||
|
|
||||||
in.beginObject();
|
in.beginObject();
|
||||||
@@ -64,7 +64,7 @@ class VaultSettingsJsonAdapter {
|
|||||||
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
|
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
|
||||||
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
|
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
|
||||||
case "mountFlags" -> mountFlags = in.nextString();
|
case "mountFlags" -> mountFlags = in.nextString();
|
||||||
case "filenameLengthLimit" -> filenameLengthLimit = in.nextInt();
|
case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
|
||||||
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
|
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
|
||||||
default -> {
|
default -> {
|
||||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||||
@@ -88,7 +88,7 @@ class VaultSettingsJsonAdapter {
|
|||||||
vaultSettings.customMountPath().set(customMountPath);
|
vaultSettings.customMountPath().set(customMountPath);
|
||||||
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
|
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
|
||||||
vaultSettings.mountFlags().set(mountFlags);
|
vaultSettings.mountFlags().set(mountFlags);
|
||||||
vaultSettings.filenameLengthLimit().set(filenameLengthLimit);
|
vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
|
||||||
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
|
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
|
||||||
return vaultSettings;
|
return vaultSettings;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ public abstract class AbstractVolume implements Volume {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected Path determineMountPoint() throws InvalidMountPointException {
|
protected Path determineMountPoint() throws InvalidMountPointException {
|
||||||
for (var chooser : Iterables.filter(choosers, c -> c.isApplicable(this))) {
|
var applicableChoosers = Iterables.filter(choosers, c -> c.isApplicable(this));
|
||||||
|
for (var chooser : applicableChoosers) {
|
||||||
Optional<Path> chosenPath = chooser.chooseMountPoint(this);
|
Optional<Path> chosenPath = chooser.chooseMountPoint(this);
|
||||||
if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint
|
if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint
|
||||||
continue;
|
continue;
|
||||||
@@ -29,7 +30,7 @@ public abstract class AbstractVolume implements Volume {
|
|||||||
this.usedChooser = chooser;
|
this.usedChooser = chooser;
|
||||||
return chosenPath.get();
|
return chosenPath.get();
|
||||||
}
|
}
|
||||||
throw new InvalidMountPointException("No feasible MountPoint found!");
|
throw new InvalidMountPointException(String.format("No feasible MountPoint found by choosers: %s", applicableChoosers));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void cleanupMountPoint() {
|
protected void cleanupMountPoint() {
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ import org.cryptomator.common.mountpoint.MountPointChooser;
|
|||||||
import org.cryptomator.common.settings.VaultSettings;
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
import org.cryptomator.common.settings.VolumeImpl;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||||
|
import org.cryptomator.frontend.dokany.DokanyMountFailedException;
|
||||||
import org.cryptomator.frontend.dokany.Mount;
|
import org.cryptomator.frontend.dokany.Mount;
|
||||||
import org.cryptomator.frontend.dokany.MountFactory;
|
import org.cryptomator.frontend.dokany.MountFactory;
|
||||||
import org.cryptomator.frontend.dokany.MountFailedException;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import java.util.SortedSet;
|
import java.util.function.Consumer;
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
|
|
||||||
public class DokanyVolume extends AbstractVolume {
|
public class DokanyVolume extends AbstractVolume {
|
||||||
|
|
||||||
@@ -23,15 +22,13 @@ public class DokanyVolume extends AbstractVolume {
|
|||||||
private static final String FS_TYPE_NAME = "CryptomatorFS";
|
private static final String FS_TYPE_NAME = "CryptomatorFS";
|
||||||
|
|
||||||
private final VaultSettings vaultSettings;
|
private final VaultSettings vaultSettings;
|
||||||
private final MountFactory mountFactory;
|
|
||||||
|
|
||||||
private Mount mount;
|
private Mount mount;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
||||||
super(choosers);
|
super(choosers);
|
||||||
this.vaultSettings = vaultSettings;
|
this.vaultSettings = vaultSettings;
|
||||||
this.mountFactory = new MountFactory(executorService);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -40,12 +37,11 @@ public class DokanyVolume extends AbstractVolume {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
|
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
||||||
this.mountPoint = determineMountPoint();
|
this.mountPoint = determineMountPoint();
|
||||||
String mountName = vaultSettings.mountName().get();
|
|
||||||
try {
|
try {
|
||||||
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip());
|
this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction);
|
||||||
} catch (MountFailedException e) {
|
} catch (DokanyMountFailedException e) {
|
||||||
if (vaultSettings.getCustomMountPath().isPresent()) {
|
if (vaultSettings.getCustomMountPath().isPresent()) {
|
||||||
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
|
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
|
||||||
}
|
}
|
||||||
@@ -54,19 +50,35 @@ public class DokanyVolume extends AbstractVolume {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reveal() throws VolumeException {
|
public void reveal(Revealer revealer) throws VolumeException {
|
||||||
boolean success = mount.reveal();
|
try {
|
||||||
if (!success) {
|
mount.reveal(revealer::reveal);
|
||||||
throw new VolumeException("Reveal failed.");
|
} catch (Exception e) {
|
||||||
|
throw new VolumeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unmount() {
|
public void unmount() throws VolumeException {
|
||||||
mount.close();
|
try {
|
||||||
|
mount.unmount();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
throw new VolumeException("Unmount Failed.", e);
|
||||||
|
}
|
||||||
cleanupMountPoint();
|
cleanupMountPoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unmountForced() {
|
||||||
|
mount.unmountForced();
|
||||||
|
cleanupMountPoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsForcedUnmount() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSupported() {
|
public boolean isSupported() {
|
||||||
return DokanyVolume.isSupportedStatic();
|
return DokanyVolume.isSupportedStatic();
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
|||||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
import org.cryptomator.common.settings.VolumeImpl;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||||
import org.cryptomator.frontend.fuse.mount.CommandFailedException;
|
|
||||||
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
|
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
|
||||||
|
import org.cryptomator.frontend.fuse.mount.FuseMountException;
|
||||||
import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
|
import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
|
||||||
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
|
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
|
||||||
import org.cryptomator.frontend.fuse.mount.Mount;
|
import org.cryptomator.frontend.fuse.mount.Mount;
|
||||||
@@ -20,7 +20,7 @@ import javax.inject.Named;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.SortedSet;
|
import java.util.function.Consumer;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class FuseVolume extends AbstractVolume {
|
public class FuseVolume extends AbstractVolume {
|
||||||
@@ -36,20 +36,21 @@ public class FuseVolume extends AbstractVolume {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
|
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
||||||
this.mountPoint = determineMountPoint();
|
this.mountPoint = determineMountPoint();
|
||||||
|
mount(fs.getPath("/"), mountFlags, onExitAction);
|
||||||
mount(fs.getPath("/"), mountFlags);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mount(Path root, String mountFlags) throws VolumeException {
|
private void mount(Path root, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
|
||||||
try {
|
try {
|
||||||
Mounter mounter = FuseMountFactory.getMounter();
|
Mounter mounter = FuseMountFactory.getMounter();
|
||||||
EnvironmentVariables envVars = EnvironmentVariables.create() //
|
EnvironmentVariables envVars = EnvironmentVariables.create() //
|
||||||
.withFlags(splitFlags(mountFlags)).withMountPoint(mountPoint) //
|
.withFlags(splitFlags(mountFlags)) //
|
||||||
|
.withMountPoint(mountPoint) //
|
||||||
|
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
|
||||||
.build();
|
.build();
|
||||||
this.mount = mounter.mount(root, envVars);
|
this.mount = mounter.mount(root, envVars, onExitAction);
|
||||||
} catch (CommandFailedException | FuseNotSupportedException e) {
|
} catch ( FuseMountException | FuseNotSupportedException e) {
|
||||||
throw new VolumeException("Unable to mount Filesystem", e);
|
throw new VolumeException("Unable to mount Filesystem", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,11 +74,10 @@ public class FuseVolume extends AbstractVolume {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reveal() throws VolumeException {
|
public void reveal(Revealer revealer) throws VolumeException {
|
||||||
try {
|
try {
|
||||||
mount.revealInFileManager();
|
mount.reveal(revealer::reveal);
|
||||||
} catch (CommandFailedException e) {
|
} catch (Exception e) {
|
||||||
LOG.debug("Revealing the vault in file manger failed: " + e.getMessage());
|
|
||||||
throw new VolumeException(e);
|
throw new VolumeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,8 +91,7 @@ public class FuseVolume extends AbstractVolume {
|
|||||||
public synchronized void unmountForced() throws VolumeException {
|
public synchronized void unmountForced() throws VolumeException {
|
||||||
try {
|
try {
|
||||||
mount.unmountForced();
|
mount.unmountForced();
|
||||||
mount.close();
|
} catch (FuseMountException e) {
|
||||||
} catch (CommandFailedException e) {
|
|
||||||
throw new VolumeException(e);
|
throw new VolumeException(e);
|
||||||
}
|
}
|
||||||
cleanupMountPoint();
|
cleanupMountPoint();
|
||||||
@@ -102,8 +101,7 @@ public class FuseVolume extends AbstractVolume {
|
|||||||
public synchronized void unmount() throws VolumeException {
|
public synchronized void unmount() throws VolumeException {
|
||||||
try {
|
try {
|
||||||
mount.unmount();
|
mount.unmount();
|
||||||
mount.close();
|
} catch (FuseMountException e) {
|
||||||
} catch (CommandFailedException e) {
|
|
||||||
throw new VolumeException(e);
|
throw new VolumeException(e);
|
||||||
}
|
}
|
||||||
cleanupMountPoint();
|
cleanupMountPoint();
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
|
public class LockNotCompletedException extends Exception {
|
||||||
|
|
||||||
|
public LockNotCompletedException(String reason) {
|
||||||
|
super(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LockNotCompletedException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ package org.cryptomator.common.vaults;
|
|||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
import org.cryptomator.common.LazyInitializer;
|
|
||||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
import org.cryptomator.common.vaults.Volume.VolumeException;
|
||||||
@@ -18,10 +17,12 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
|
|||||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||||
import org.cryptomator.cryptofs.common.Constants;
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
|
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
|
||||||
import org.cryptomator.cryptolib.api.CryptoException;
|
import org.cryptomator.cryptolib.api.CryptoException;
|
||||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -36,28 +37,29 @@ import javafx.beans.property.BooleanProperty;
|
|||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.NoSuchFileException;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
|
||||||
|
|
||||||
@PerVault
|
@PerVault
|
||||||
public class Vault {
|
public class Vault {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
|
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
|
||||||
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
|
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
|
||||||
|
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
|
||||||
|
|
||||||
private final VaultSettings vaultSettings;
|
private final VaultSettings vaultSettings;
|
||||||
private final Provider<Volume> volumeProvider;
|
private final Provider<Volume> volumeProvider;
|
||||||
private final StringBinding defaultMountFlags;
|
private final StringBinding defaultMountFlags;
|
||||||
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
|
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
|
||||||
private final ObjectProperty<VaultState> state;
|
private final VaultState state;
|
||||||
private final ObjectProperty<Exception> lastKnownException;
|
private final ObjectProperty<Exception> lastKnownException;
|
||||||
private final VaultStats stats;
|
private final VaultStats stats;
|
||||||
private final StringBinding displayName;
|
private final StringBinding displayName;
|
||||||
@@ -75,7 +77,7 @@ public class Vault {
|
|||||||
private volatile Volume volume;
|
private volatile Volume volume;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, ObjectProperty<VaultState> state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
|
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
|
||||||
this.vaultSettings = vaultSettings;
|
this.vaultSettings = vaultSettings;
|
||||||
this.volumeProvider = volumeProvider;
|
this.volumeProvider = volumeProvider;
|
||||||
this.defaultMountFlags = defaultMountFlags;
|
this.defaultMountFlags = defaultMountFlags;
|
||||||
@@ -100,44 +102,37 @@ public class Vault {
|
|||||||
// Commands
|
// Commands
|
||||||
// ********************************************************************************/
|
// ********************************************************************************/
|
||||||
|
|
||||||
private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
|
private CryptoFileSystem createCryptoFileSystem(MasterkeyLoader keyLoader) throws IOException, MasterkeyLoadingFailedException {
|
||||||
return LazyInitializer.initializeLazily(cryptoFileSystem, () -> unlockCryptoFileSystem(passphrase), IOException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
|
|
||||||
Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
|
Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
|
||||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
if (vaultSettings.usesReadOnlyMode().get()) {
|
||||||
flags.add(FileSystemFlags.READONLY);
|
flags.add(FileSystemFlags.READONLY);
|
||||||
|
} else if(vaultSettings.maxCleartextFilenameLength().get() == -1) {
|
||||||
|
LOG.debug("Determining cleartext filename length limitations...");
|
||||||
|
var checker = new FileSystemCapabilityChecker();
|
||||||
|
int shorteningThreshold = getUnverifiedVaultConfig().allegedShorteningThreshold();
|
||||||
|
int ciphertextLimit = checker.determineSupportedCiphertextFileNameLength(getPath());
|
||||||
|
if (ciphertextLimit < shorteningThreshold) {
|
||||||
|
int cleartextLimit = checker.determineSupportedCleartextFileNameLength(getPath());
|
||||||
|
vaultSettings.maxCleartextFilenameLength().set(cleartextLimit);
|
||||||
|
} else {
|
||||||
|
vaultSettings.maxCleartextFilenameLength().setValue(UNLIMITED_FILENAME_LENGTH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (vaultSettings.filenameLengthLimit().get() == -1) {
|
|
||||||
LOG.debug("Determining file name length limitations...");
|
if (vaultSettings.maxCleartextFilenameLength().get() < UNLIMITED_FILENAME_LENGTH) {
|
||||||
int limit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(getPath());
|
LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength().get());
|
||||||
vaultSettings.filenameLengthLimit().set(limit);
|
|
||||||
LOG.info("Storing file name length limit of {}", limit);
|
|
||||||
}
|
}
|
||||||
assert vaultSettings.filenameLengthLimit().get() > 0;
|
|
||||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||||
.withPassphrase(passphrase) //
|
.withKeyLoader(keyLoader) //
|
||||||
.withFlags(flags) //
|
.withFlags(flags) //
|
||||||
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength().get()) //
|
||||||
.withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) //
|
|
||||||
.withMaxNameLength(vaultSettings.filenameLengthLimit().get()) //
|
|
||||||
.build();
|
.build();
|
||||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
private void destroyCryptoFileSystem() {
|
||||||
CryptoFileSystem fs = getCryptoFileSystem(passphrase);
|
LOG.trace("Trying to close associated CryptoFS...");
|
||||||
volume = volumeProvider.get();
|
|
||||||
volume.mount(fs, getEffectiveMountFlags());
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void lock(boolean forced) throws VolumeException {
|
|
||||||
if (forced && volume.supportsForcedUnmount()) {
|
|
||||||
volume.unmountForced();
|
|
||||||
} else {
|
|
||||||
volume.unmount();
|
|
||||||
}
|
|
||||||
CryptoFileSystem fs = cryptoFileSystem.getAndSet(null);
|
CryptoFileSystem fs = cryptoFileSystem.getAndSet(null);
|
||||||
if (fs != null) {
|
if (fs != null) {
|
||||||
try {
|
try {
|
||||||
@@ -148,24 +143,67 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reveal() throws VolumeException {
|
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
||||||
volume.reveal();
|
if (cryptoFileSystem.get() != null) {
|
||||||
|
throw new IllegalStateException("Already unlocked.");
|
||||||
|
}
|
||||||
|
CryptoFileSystem fs = createCryptoFileSystem(keyLoader);
|
||||||
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
cryptoFileSystem.set(fs);
|
||||||
|
volume = volumeProvider.get();
|
||||||
|
volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit);
|
||||||
|
success = true;
|
||||||
|
} finally {
|
||||||
|
if (!success) {
|
||||||
|
destroyCryptoFileSystem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void lockOnVolumeExit(Throwable t) {
|
||||||
|
LOG.info("Unmounted vault '{}'", getDisplayName());
|
||||||
|
destroyCryptoFileSystem();
|
||||||
|
state.set(VaultState.Value.LOCKED);
|
||||||
|
if (t != null) {
|
||||||
|
LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException {
|
||||||
|
//initiate unmount
|
||||||
|
if (forced && volume.supportsForcedUnmount()) {
|
||||||
|
volume.unmountForced();
|
||||||
|
} else {
|
||||||
|
volume.unmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
//wait for lockOnVolumeExit to be executed
|
||||||
|
try {
|
||||||
|
boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS);
|
||||||
|
if (!locked) {
|
||||||
|
throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress.");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new LockNotCompletedException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
|
||||||
|
volume.reveal(vaultRevealer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ******************************************************************************
|
// ******************************************************************************
|
||||||
// Observable Properties
|
// Observable Properties
|
||||||
// *******************************************************************************
|
// *******************************************************************************
|
||||||
|
|
||||||
public ObjectProperty<VaultState> stateProperty() {
|
public VaultState stateProperty() {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VaultState getState() {
|
public VaultState.Value getState() {
|
||||||
return state.get();
|
return state.getValue();
|
||||||
}
|
|
||||||
|
|
||||||
public void setState(VaultState value) {
|
|
||||||
state.setValue(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectProperty<Exception> lastKnownExceptionProperty() {
|
public ObjectProperty<Exception> lastKnownExceptionProperty() {
|
||||||
@@ -185,7 +223,7 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLocked() {
|
public boolean isLocked() {
|
||||||
return state.get() == VaultState.LOCKED;
|
return state.get() == VaultState.Value.LOCKED;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanBinding processingProperty() {
|
public BooleanBinding processingProperty() {
|
||||||
@@ -193,7 +231,7 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isProcessing() {
|
public boolean isProcessing() {
|
||||||
return state.get() == VaultState.PROCESSING;
|
return state.get() == VaultState.Value.PROCESSING;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanBinding unlockedProperty() {
|
public BooleanBinding unlockedProperty() {
|
||||||
@@ -201,7 +239,7 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isUnlocked() {
|
public boolean isUnlocked() {
|
||||||
return state.get() == VaultState.UNLOCKED;
|
return state.get() == VaultState.Value.UNLOCKED;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanBinding missingProperty() {
|
public BooleanBinding missingProperty() {
|
||||||
@@ -209,7 +247,7 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isMissing() {
|
public boolean isMissing() {
|
||||||
return state.get() == VaultState.MISSING;
|
return state.get() == VaultState.Value.MISSING;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanBinding needsMigrationProperty() {
|
public BooleanBinding needsMigrationProperty() {
|
||||||
@@ -217,7 +255,7 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isNeedsMigration() {
|
public boolean isNeedsMigration() {
|
||||||
return state.get() == VaultState.NEEDS_MIGRATION;
|
return state.get() == VaultState.Value.NEEDS_MIGRATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanBinding unknownErrorProperty() {
|
public BooleanBinding unknownErrorProperty() {
|
||||||
@@ -225,7 +263,7 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isUnknownError() {
|
public boolean isUnknownError() {
|
||||||
return state.get() == VaultState.ERROR;
|
return state.get() == VaultState.Value.ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringBinding displayNameProperty() {
|
public StringBinding displayNameProperty() {
|
||||||
@@ -241,7 +279,7 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getAccessPoint() {
|
public String getAccessPoint() {
|
||||||
if (state.get() == VaultState.UNLOCKED) {
|
if (state.getValue() == VaultState.Value.UNLOCKED) {
|
||||||
assert volume != null;
|
assert volume != null;
|
||||||
return volume.getMountPoint().orElse(Path.of("")).toString();
|
return volume.getMountPoint().orElse(Path.of("")).toString();
|
||||||
} else {
|
} else {
|
||||||
@@ -289,6 +327,12 @@ public class Vault {
|
|||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UnverifiedVaultConfig getUnverifiedVaultConfig() throws IOException {
|
||||||
|
Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME);
|
||||||
|
String token = Files.readString(configPath, StandardCharsets.US_ASCII);
|
||||||
|
return VaultConfig.decode(token);
|
||||||
|
}
|
||||||
|
|
||||||
public Observable[] observables() {
|
public Observable[] observables() {
|
||||||
return new Observable[]{state};
|
return new Observable[]{state};
|
||||||
}
|
}
|
||||||
@@ -345,8 +389,7 @@ public class Vault {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
if (obj instanceof Vault && obj.getClass().equals(this.getClass())) {
|
if (obj instanceof Vault other && obj.getClass().equals(this.getClass())) {
|
||||||
final Vault other = (Vault) obj;
|
|
||||||
return Objects.equals(this.vaultSettings, other.vaultSettings);
|
return Objects.equals(this.vaultSettings, other.vaultSettings);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public interface VaultComponent {
|
|||||||
Builder vaultSettings(VaultSettings vaultSettings);
|
Builder vaultSettings(VaultSettings vaultSettings);
|
||||||
|
|
||||||
@BindsInstance
|
@BindsInstance
|
||||||
Builder initialVaultState(VaultState vaultState);
|
Builder initialVaultState(VaultState.Value vaultState);
|
||||||
|
|
||||||
@BindsInstance
|
@BindsInstance
|
||||||
Builder initialErrorCause(@Nullable @Named("lastKnownException") Exception initialErrorCause);
|
Builder initialErrorCause(@Nullable @Named("lastKnownException") Exception initialErrorCause);
|
||||||
|
|||||||
+2
-2
@@ -22,10 +22,10 @@ class VaultListChangeListener implements ListChangeListener<Vault> {
|
|||||||
public void onChanged(Change<? extends Vault> c) {
|
public void onChanged(Change<? extends Vault> c) {
|
||||||
while (c.next()) {
|
while (c.next()) {
|
||||||
if (c.wasAdded()) {
|
if (c.wasAdded()) {
|
||||||
List<VaultSettings> addedSettings = c.getAddedSubList().stream().map(Vault::getVaultSettings).collect(Collectors.toList());
|
List<VaultSettings> addedSettings = c.getAddedSubList().stream().map(Vault::getVaultSettings).toList();
|
||||||
vaultSettingsList.addAll(c.getFrom(), addedSettings);
|
vaultSettingsList.addAll(c.getFrom(), addedSettings);
|
||||||
} else if (c.wasRemoved()) {
|
} else if (c.wasRemoved()) {
|
||||||
List<VaultSettings> removedSettings = c.getRemoved().stream().map(Vault::getVaultSettings).collect(Collectors.toList());
|
List<VaultSettings> removedSettings = c.getRemoved().stream().map(Vault::getVaultSettings).toList();
|
||||||
vaultSettingsList.removeAll(removedSettings);
|
vaultSettingsList.removeAll(removedSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ package org.cryptomator.common.vaults;
|
|||||||
import org.cryptomator.common.settings.Settings;
|
import org.cryptomator.common.settings.Settings;
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||||
|
import org.cryptomator.cryptofs.DirStructure;
|
||||||
import org.cryptomator.cryptofs.migration.Migrators;
|
import org.cryptomator.cryptofs.migration.Migrators;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -20,14 +21,16 @@ import javax.inject.Singleton;
|
|||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.NoSuchFileException;
|
import java.nio.file.NoSuchFileException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||||
|
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
|
||||||
|
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class VaultListManager {
|
public class VaultListManager {
|
||||||
@@ -52,19 +55,18 @@ public class VaultListManager {
|
|||||||
return vaultList;
|
return vaultList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vault add(Path pathToVault) throws NoSuchFileException {
|
public Vault add(Path pathToVault) throws IOException {
|
||||||
Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath();
|
Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath();
|
||||||
if (!CryptoFileSystemProvider.containsVault(normalizedPathToVault, MASTERKEY_FILENAME)) {
|
if (CryptoFileSystemProvider.checkDirStructureForVault(normalizedPathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) == DirStructure.UNRELATED) {
|
||||||
throw new NoSuchFileException(normalizedPathToVault.toString(), null, "Not a vault directory");
|
throw new NoSuchFileException(normalizedPathToVault.toString(), null, "Not a vault directory");
|
||||||
}
|
}
|
||||||
Optional<Vault> alreadyExistingVault = get(normalizedPathToVault);
|
|
||||||
if (alreadyExistingVault.isPresent()) {
|
return get(normalizedPathToVault) //
|
||||||
return alreadyExistingVault.get();
|
.orElseGet(() -> {
|
||||||
} else {
|
Vault newVault = create(newVaultSettings(normalizedPathToVault));
|
||||||
Vault newVault = create(newVaultSettings(normalizedPathToVault));
|
vaultList.add(newVault);
|
||||||
vaultList.add(newVault);
|
return newVault;
|
||||||
return newVault;
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private VaultSettings newVaultSettings(Path path) {
|
private VaultSettings newVaultSettings(Path path) {
|
||||||
@@ -79,7 +81,7 @@ public class VaultListManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void addAll(Collection<VaultSettings> vaultSettings) {
|
private void addAll(Collection<VaultSettings> vaultSettings) {
|
||||||
Collection<Vault> vaults = vaultSettings.stream().map(this::create).collect(Collectors.toList());
|
Collection<Vault> vaults = vaultSettings.stream().map(this::create).toList();
|
||||||
vaultList.addAll(vaults);
|
vaultList.addAll(vaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,43 +96,45 @@ public class VaultListManager {
|
|||||||
private Vault create(VaultSettings vaultSettings) {
|
private Vault create(VaultSettings vaultSettings) {
|
||||||
VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings);
|
VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings);
|
||||||
try {
|
try {
|
||||||
VaultState vaultState = determineVaultState(vaultSettings.path().get());
|
VaultState.Value vaultState = determineVaultState(vaultSettings.path().get());
|
||||||
compBuilder.initialVaultState(vaultState);
|
compBuilder.initialVaultState(vaultState);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e);
|
LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e);
|
||||||
compBuilder.initialVaultState(VaultState.ERROR);
|
compBuilder.initialVaultState(ERROR);
|
||||||
compBuilder.initialErrorCause(e);
|
compBuilder.initialErrorCause(e);
|
||||||
}
|
}
|
||||||
return compBuilder.build().vault();
|
return compBuilder.build().vault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static VaultState redetermineVaultState(Vault vault) {
|
public static VaultState.Value redetermineVaultState(Vault vault) {
|
||||||
VaultState previousState = vault.getState();
|
VaultState state = vault.stateProperty();
|
||||||
|
VaultState.Value previousState = state.getValue();
|
||||||
return switch (previousState) {
|
return switch (previousState) {
|
||||||
case LOCKED, NEEDS_MIGRATION, MISSING -> {
|
case LOCKED, NEEDS_MIGRATION, MISSING -> {
|
||||||
try {
|
try {
|
||||||
VaultState determinedState = determineVaultState(vault.getPath());
|
var determinedState = determineVaultState(vault.getPath());
|
||||||
vault.setState(determinedState);
|
state.set(determinedState);
|
||||||
yield determinedState;
|
yield determinedState;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
|
LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
|
||||||
vault.setState(VaultState.ERROR);
|
state.set(ERROR);
|
||||||
vault.setLastKnownException(e);
|
vault.setLastKnownException(e);
|
||||||
yield VaultState.ERROR;
|
yield ERROR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ERROR, UNLOCKED, PROCESSING -> previousState;
|
case ERROR, UNLOCKED, PROCESSING -> previousState;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static VaultState determineVaultState(Path pathToVault) throws IOException {
|
private static VaultState.Value determineVaultState(Path pathToVault) throws IOException {
|
||||||
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
|
if (!Files.exists(pathToVault)) {
|
||||||
return VaultState.MISSING;
|
return VaultState.Value.MISSING;
|
||||||
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
|
|
||||||
return VaultState.NEEDS_MIGRATION;
|
|
||||||
} else {
|
|
||||||
return VaultState.LOCKED;
|
|
||||||
}
|
}
|
||||||
|
return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) {
|
||||||
|
case VAULT -> VaultState.Value.LOCKED;
|
||||||
|
case UNRELATED -> VaultState.Value.MISSING;
|
||||||
|
case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? VaultState.Value.NEEDS_MIGRATION : VaultState.Value.MISSING;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,12 +40,6 @@ public class VaultModule {
|
|||||||
return new AtomicReference<>();
|
return new AtomicReference<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@PerVault
|
|
||||||
public ObjectProperty<VaultState> provideVaultState(VaultState initialState) {
|
|
||||||
return new SimpleObjectProperty<>(initialState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Named("lastKnownException")
|
@Named("lastKnownException")
|
||||||
@PerVault
|
@PerVault
|
||||||
@@ -53,7 +47,6 @@ public class VaultModule {
|
|||||||
return new SimpleObjectProperty<>(initialErrorCause);
|
return new SimpleObjectProperty<>(initialErrorCause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
|
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
|
||||||
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
||||||
@@ -105,7 +98,6 @@ public class VaultModule {
|
|||||||
flags.append(" -oatomic_o_trunc");
|
flags.append(" -oatomic_o_trunc");
|
||||||
flags.append(" -oauto_xattr");
|
flags.append(" -oauto_xattr");
|
||||||
flags.append(" -oauto_cache");
|
flags.append(" -oauto_cache");
|
||||||
flags.append(" -omodules=iconv,from_code=UTF-8,to_code=UTF-8-MAC"); // show files names in Unicode NFD encoding
|
|
||||||
flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
|
flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
|
||||||
flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
|
flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
|
||||||
|
|
||||||
@@ -156,7 +148,7 @@ public class VaultModule {
|
|||||||
//See: https://github.com/billziss-gh/winfsp/issues/319
|
//See: https://github.com/billziss-gh/winfsp/issues/319
|
||||||
if (!readOnly.get()) {
|
if (!readOnly.get()) {
|
||||||
flags.append(" -ouid=-1");
|
flags.append(" -ouid=-1");
|
||||||
flags.append(" -ogid=-1");
|
flags.append(" -ogid=11");
|
||||||
}
|
}
|
||||||
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
|
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
|
||||||
//Dokany requires this option to be set, WinFSP doesn't seem to share this peculiarity,
|
//Dokany requires this option to be set, WinFSP doesn't seem to share this peculiarity,
|
||||||
|
|||||||
@@ -1,34 +1,141 @@
|
|||||||
package org.cryptomator.common.vaults;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
public enum VaultState {
|
import com.google.common.base.Preconditions;
|
||||||
/**
|
import org.slf4j.Logger;
|
||||||
* No vault found at the provided path
|
import org.slf4j.LoggerFactory;
|
||||||
*/
|
|
||||||
MISSING,
|
import javax.inject.Inject;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.value.ObservableObjectValue;
|
||||||
|
import javafx.beans.value.ObservableValueBase;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.concurrent.locks.Condition;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
@PerVault
|
||||||
|
public class VaultState extends ObservableValueBase<VaultState.Value> implements ObservableObjectValue<VaultState.Value> {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(VaultState.class);
|
||||||
|
|
||||||
|
public enum Value {
|
||||||
|
/**
|
||||||
|
* No vault found at the provided path
|
||||||
|
*/
|
||||||
|
MISSING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault requires migration to a newer vault format
|
||||||
|
*/
|
||||||
|
NEEDS_MIGRATION,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault ready to be unlocked
|
||||||
|
*/
|
||||||
|
LOCKED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault in transition between two other states
|
||||||
|
*/
|
||||||
|
PROCESSING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault is unlocked
|
||||||
|
*/
|
||||||
|
UNLOCKED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unknown state due to preceeding unrecoverable exceptions.
|
||||||
|
*/
|
||||||
|
ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final AtomicReference<Value> value;
|
||||||
|
private final Lock lock = new ReentrantLock();
|
||||||
|
private final Condition valueChanged = lock.newCondition();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public VaultState(VaultState.Value initialValue) {
|
||||||
|
this.value = new AtomicReference<>(initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Value get() {
|
||||||
|
return getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Value getValue() {
|
||||||
|
return value.get();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vault requires migration to a newer vault format
|
* Transitions from <code>fromState</code> to <code>toState</code>.
|
||||||
|
*
|
||||||
|
* @param fromState Previous state
|
||||||
|
* @param toState New state
|
||||||
|
* @return <code>true</code> if successful
|
||||||
*/
|
*/
|
||||||
NEEDS_MIGRATION,
|
public boolean transition(Value fromState, Value toState) {
|
||||||
|
Preconditions.checkArgument(fromState != toState, "fromState must be different than toState");
|
||||||
|
boolean success = value.compareAndSet(fromState, toState);
|
||||||
|
if (success) {
|
||||||
|
fireValueChangedEvent();
|
||||||
|
} else {
|
||||||
|
LOG.debug("Failed transiting into state {}: Expected state was not{}.", fromState, toState);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(Value newState) {
|
||||||
|
var oldState = value.getAndSet(newState);
|
||||||
|
if (oldState != newState) {
|
||||||
|
fireValueChangedEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vault ready to be unlocked
|
* Waits for the specified time, until the desired state is reached.
|
||||||
|
*
|
||||||
|
* @param desiredState what state to wait for
|
||||||
|
* @param time the maximum time to wait
|
||||||
|
* @param unit the time unit of the {@code time} argument
|
||||||
|
* @return {@code false} if the waiting time detectably elapsed before reaching {@code desiredState}
|
||||||
|
* @throws InterruptedException if the current thread is interrupted
|
||||||
*/
|
*/
|
||||||
LOCKED,
|
public boolean awaitState(Value desiredState, long time, TimeUnit unit) throws InterruptedException {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
long remaining = TimeUnit.NANOSECONDS.convert(time, unit);
|
||||||
|
while (value.get() != desiredState) {
|
||||||
|
if (remaining <= 0L) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
remaining = valueChanged.awaitNanos(remaining);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
private void signal() {
|
||||||
* Vault in transition between two other states
|
lock.lock();
|
||||||
*/
|
try {
|
||||||
PROCESSING,
|
valueChanged.signalAll();
|
||||||
|
} finally {
|
||||||
/**
|
lock.unlock();
|
||||||
* Vault is unlocked
|
}
|
||||||
*/
|
}
|
||||||
UNLOCKED,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unknown state due to preceeding unrecoverable exceptions.
|
|
||||||
*/
|
|
||||||
ERROR;
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void fireValueChangedEvent() {
|
||||||
|
signal();
|
||||||
|
if (Platform.isFxApplicationThread()) {
|
||||||
|
super.fireValueChangedEvent();
|
||||||
|
} else {
|
||||||
|
Platform.runLater(super::fireValueChangedEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class VaultStats {
|
|||||||
private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class);
|
private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class);
|
||||||
|
|
||||||
private final AtomicReference<CryptoFileSystem> fs;
|
private final AtomicReference<CryptoFileSystem> fs;
|
||||||
private final ObjectProperty<VaultState> state;
|
private final VaultState state;
|
||||||
private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
|
private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
|
||||||
private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
|
private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
|
||||||
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
|
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
|
||||||
@@ -41,7 +41,7 @@ public class VaultStats {
|
|||||||
private final LongProperty filesWritten = new SimpleLongProperty();
|
private final LongProperty filesWritten = new SimpleLongProperty();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
VaultStats(AtomicReference<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) {
|
VaultStats(AtomicReference<CryptoFileSystem> fs, VaultState state, ExecutorService executor) {
|
||||||
this.fs = fs;
|
this.fs = fs;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.updateService = new UpdateStatsService();
|
this.updateService = new UpdateStatsService();
|
||||||
@@ -52,13 +52,13 @@ public class VaultStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
|
private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
|
||||||
if (VaultState.UNLOCKED.equals(state.get())) {
|
if (VaultState.Value.UNLOCKED == state.get()) {
|
||||||
assert fs.get() != null;
|
assert fs.get() != null;
|
||||||
LOG.debug("start recording stats");
|
LOG.debug("start recording stats");
|
||||||
updateService.restart();
|
Platform.runLater(() -> updateService.restart());
|
||||||
} else {
|
} else {
|
||||||
LOG.debug("stop recording stats");
|
LOG.debug("stop recording stats");
|
||||||
updateService.cancel();
|
Platform.runLater(() -> updateService.cancel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,9 +34,17 @@ public interface Volume {
|
|||||||
* @param fs
|
* @param fs
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException, InvalidMountPointException;
|
void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws IOException, VolumeException, InvalidMountPointException;
|
||||||
|
|
||||||
void reveal() throws VolumeException;
|
/**
|
||||||
|
* Reveals the mounted volume.
|
||||||
|
* <p>
|
||||||
|
* The given {@code revealer} might be used to do it, but not necessarily.
|
||||||
|
*
|
||||||
|
* @param revealer An object capable of revealing the location of the mounted vault to view the content (e.g. in the default file browser).
|
||||||
|
* @throws VolumeException
|
||||||
|
*/
|
||||||
|
void reveal(Revealer revealer) throws VolumeException;
|
||||||
|
|
||||||
void unmount() throws VolumeException;
|
void unmount() throws VolumeException;
|
||||||
|
|
||||||
@@ -79,4 +89,14 @@ public interface Volume {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides and unifies the different Revealer implementations in the different nio-adapters.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
interface Revealer {
|
||||||
|
|
||||||
|
void reveal(Path p) throws VolumeException;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import java.net.InetAddress;
|
|||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class WebDavVolume implements Volume {
|
public class WebDavVolume implements Volume {
|
||||||
|
|
||||||
@@ -25,21 +27,29 @@ public class WebDavVolume implements Volume {
|
|||||||
private final Provider<WebDavServer> serverProvider;
|
private final Provider<WebDavServer> serverProvider;
|
||||||
private final VaultSettings vaultSettings;
|
private final VaultSettings vaultSettings;
|
||||||
private final Settings settings;
|
private final Settings settings;
|
||||||
|
private final WindowsDriveLetters windowsDriveLetters;
|
||||||
|
|
||||||
private WebDavServer server;
|
private WebDavServer server;
|
||||||
private WebDavServletController servlet;
|
private WebDavServletController servlet;
|
||||||
private Mounter.Mount mount;
|
private Mounter.Mount mount;
|
||||||
private Path mountPoint;
|
private Consumer<Throwable> onExitAction;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings) {
|
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
|
||||||
this.serverProvider = serverProvider;
|
this.serverProvider = serverProvider;
|
||||||
this.vaultSettings = vaultSettings;
|
this.vaultSettings = vaultSettings;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
|
this.windowsDriveLetters = windowsDriveLetters;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException {
|
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
|
||||||
|
startServlet(fs);
|
||||||
|
mountServlet();
|
||||||
|
this.onExitAction = onExitAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startServlet(CryptoFileSystem fs) {
|
||||||
if (server == null) {
|
if (server == null) {
|
||||||
server = serverProvider.get();
|
server = serverProvider.get();
|
||||||
}
|
}
|
||||||
@@ -50,32 +60,38 @@ public class WebDavVolume implements Volume {
|
|||||||
String urlConformMountName = acceptable.negate().collapseFrom(vaultSettings.mountName().get(), '_');
|
String urlConformMountName = acceptable.negate().collapseFrom(vaultSettings.mountName().get(), '_');
|
||||||
servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + urlConformMountName);
|
servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + urlConformMountName);
|
||||||
servlet.start();
|
servlet.start();
|
||||||
mount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mount() throws VolumeException {
|
private void mountServlet() throws VolumeException {
|
||||||
if (servlet == null) {
|
if (servlet == null) {
|
||||||
throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
|
throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specifc one or there is no free.
|
||||||
|
Supplier<String> driveLetterSupplier;
|
||||||
|
if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
|
||||||
|
driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null);
|
||||||
|
} else {
|
||||||
|
driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
|
||||||
|
}
|
||||||
|
|
||||||
MountParams mountParams = MountParams.create() //
|
MountParams mountParams = MountParams.create() //
|
||||||
.withWindowsDriveLetter(vaultSettings.winDriveLetter().get()) //
|
.withWindowsDriveLetter(driveLetterSupplier.get()) //
|
||||||
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
|
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
|
||||||
.withWebdavHostname(getLocalhostAliasOrNull()) //
|
.withWebdavHostname(getLocalhostAliasOrNull()) //
|
||||||
.build();
|
.build();
|
||||||
try {
|
try {
|
||||||
this.mount = servlet.mount(mountParams); // might block this thread for a while
|
this.mount = servlet.mount(mountParams); // might block this thread for a while
|
||||||
} catch (Mounter.CommandFailedException e) {
|
} catch (Mounter.CommandFailedException e) {
|
||||||
e.printStackTrace();
|
|
||||||
throw new VolumeException(e);
|
throw new VolumeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reveal() throws VolumeException {
|
public void reveal(Revealer revealer) throws VolumeException {
|
||||||
try {
|
try {
|
||||||
mount.reveal();
|
mount.reveal(revealer::reveal);
|
||||||
} catch (Mounter.CommandFailedException e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
|
||||||
throw new VolumeException(e);
|
throw new VolumeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,6 +104,7 @@ public class WebDavVolume implements Volume {
|
|||||||
throw new VolumeException(e);
|
throw new VolumeException(e);
|
||||||
}
|
}
|
||||||
cleanup();
|
cleanup();
|
||||||
|
onExitAction.accept(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -98,11 +115,12 @@ public class WebDavVolume implements Volume {
|
|||||||
throw new VolumeException(e);
|
throw new VolumeException(e);
|
||||||
}
|
}
|
||||||
cleanup();
|
cleanup();
|
||||||
|
onExitAction.accept(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Path> getMountPoint() {
|
public Optional<Path> getMountPoint() {
|
||||||
return Optional.ofNullable(mountPoint); //TODO
|
return mount.getMountPoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.junit.jupiter.api.BeforeEach;
|
|||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
@@ -20,14 +21,10 @@ class EnvironmentTest {
|
|||||||
|
|
||||||
private Environment env;
|
private Environment env;
|
||||||
|
|
||||||
@BeforeAll
|
|
||||||
static void init() {
|
|
||||||
System.setProperty("user.home", "/home/testuser");
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void initEach() {
|
void init() {
|
||||||
env = new Environment();
|
env = Mockito.spy(new Environment());
|
||||||
|
Mockito.when(env.getHomeDir()).thenReturn(Path.of("/home/testuser"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -35,7 +32,7 @@ class EnvironmentTest {
|
|||||||
public void testSettingsPath() {
|
public void testSettingsPath() {
|
||||||
System.setProperty("cryptomator.settingsPath", "~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json");
|
System.setProperty("cryptomator.settingsPath", "~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json");
|
||||||
|
|
||||||
List<Path> result = env.getSettingsPath().collect(Collectors.toList());
|
List<Path> result = env.getSettingsPath().toList();
|
||||||
MatcherAssert.assertThat(result, Matchers.hasSize(2));
|
MatcherAssert.assertThat(result, Matchers.hasSize(2));
|
||||||
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/settings.json"), //
|
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/settings.json"), //
|
||||||
Paths.get("/home/testuser/.Cryptomator/settings.json")));
|
Paths.get("/home/testuser/.Cryptomator/settings.json")));
|
||||||
@@ -46,7 +43,7 @@ class EnvironmentTest {
|
|||||||
public void testIpcPortPath() {
|
public void testIpcPortPath() {
|
||||||
System.setProperty("cryptomator.ipcPortPath", "~/.config/Cryptomator/ipcPort.bin:~/.Cryptomator/ipcPort.bin");
|
System.setProperty("cryptomator.ipcPortPath", "~/.config/Cryptomator/ipcPort.bin:~/.Cryptomator/ipcPort.bin");
|
||||||
|
|
||||||
List<Path> result = env.getIpcPortPath().collect(Collectors.toList());
|
List<Path> result = env.getIpcPortPath().toList();
|
||||||
MatcherAssert.assertThat(result, Matchers.hasSize(2));
|
MatcherAssert.assertThat(result, Matchers.hasSize(2));
|
||||||
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/ipcPort.bin"), //
|
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/ipcPort.bin"), //
|
||||||
Paths.get("/home/testuser/.Cryptomator/ipcPort.bin")));
|
Paths.get("/home/testuser/.Cryptomator/ipcPort.bin")));
|
||||||
@@ -57,7 +54,7 @@ class EnvironmentTest {
|
|||||||
public void testKeychainPath() {
|
public void testKeychainPath() {
|
||||||
System.setProperty("cryptomator.keychainPath", "~/AppData/Roaming/Cryptomator/keychain.json");
|
System.setProperty("cryptomator.keychainPath", "~/AppData/Roaming/Cryptomator/keychain.json");
|
||||||
|
|
||||||
List<Path> result = env.getKeychainPath().collect(Collectors.toList());
|
List<Path> result = env.getKeychainPath().toList();
|
||||||
MatcherAssert.assertThat(result, Matchers.hasSize(1));
|
MatcherAssert.assertThat(result, Matchers.hasSize(1));
|
||||||
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/AppData/Roaming/Cryptomator/keychain.json")));
|
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/AppData/Roaming/Cryptomator/keychain.json")));
|
||||||
}
|
}
|
||||||
@@ -91,7 +88,7 @@ class EnvironmentTest {
|
|||||||
@DisplayName("test.path.property=")
|
@DisplayName("test.path.property=")
|
||||||
public void testEmptyList() {
|
public void testEmptyList() {
|
||||||
System.setProperty("test.path.property", "");
|
System.setProperty("test.path.property", "");
|
||||||
List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList());
|
List<Path> result = env.getPaths("test.path.property").toList();
|
||||||
|
|
||||||
MatcherAssert.assertThat(result, Matchers.hasSize(0));
|
MatcherAssert.assertThat(result, Matchers.hasSize(0));
|
||||||
}
|
}
|
||||||
@@ -100,7 +97,7 @@ class EnvironmentTest {
|
|||||||
@DisplayName("test.path.property=/foo/bar/test")
|
@DisplayName("test.path.property=/foo/bar/test")
|
||||||
public void testSingleAbsolutePath() {
|
public void testSingleAbsolutePath() {
|
||||||
System.setProperty("test.path.property", "/foo/bar/test");
|
System.setProperty("test.path.property", "/foo/bar/test");
|
||||||
List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList());
|
List<Path> result = env.getPaths("test.path.property").toList();
|
||||||
|
|
||||||
MatcherAssert.assertThat(result, Matchers.hasSize(1));
|
MatcherAssert.assertThat(result, Matchers.hasSize(1));
|
||||||
MatcherAssert.assertThat(result, Matchers.hasItem(Paths.get("/foo/bar/test")));
|
MatcherAssert.assertThat(result, Matchers.hasItem(Paths.get("/foo/bar/test")));
|
||||||
@@ -110,7 +107,7 @@ class EnvironmentTest {
|
|||||||
@DisplayName("test.path.property=~/test")
|
@DisplayName("test.path.property=~/test")
|
||||||
public void testSingleHomeRelativePath() {
|
public void testSingleHomeRelativePath() {
|
||||||
System.setProperty("test.path.property", "~/test");
|
System.setProperty("test.path.property", "~/test");
|
||||||
List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList());
|
List<Path> result = env.getPaths("test.path.property").toList();
|
||||||
|
|
||||||
MatcherAssert.assertThat(result, Matchers.hasSize(1));
|
MatcherAssert.assertThat(result, Matchers.hasSize(1));
|
||||||
MatcherAssert.assertThat(result, Matchers.hasItem(Paths.get("/home/testuser/test")));
|
MatcherAssert.assertThat(result, Matchers.hasItem(Paths.get("/home/testuser/test")));
|
||||||
@@ -120,7 +117,7 @@ class EnvironmentTest {
|
|||||||
@DisplayName("test.path.property=~/test:~/test2:/foo/bar/test")
|
@DisplayName("test.path.property=~/test:~/test2:/foo/bar/test")
|
||||||
public void testMultiplePaths() {
|
public void testMultiplePaths() {
|
||||||
System.setProperty("test.path.property", "~/test:~/test2:/foo/bar/test");
|
System.setProperty("test.path.property", "~/test:~/test2:/foo/bar/test");
|
||||||
List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList());
|
List<Path> result = env.getPaths("test.path.property").toList();
|
||||||
|
|
||||||
MatcherAssert.assertThat(result, Matchers.hasSize(3));
|
MatcherAssert.assertThat(result, Matchers.hasSize(3));
|
||||||
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/test"), //
|
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/test"), //
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import java.util.Optional;
|
|||||||
|
|
||||||
class LicenseCheckerTest {
|
class LicenseCheckerTest {
|
||||||
|
|
||||||
private static final String PUBLIC_KEY = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ" //
|
private static final String PUBLIC_KEY = """
|
||||||
+ "PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47" //
|
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ\
|
||||||
+ "6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM" //
|
PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47\
|
||||||
+ "Al8G7CqwoJOsW7Kddns=";
|
6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM\
|
||||||
|
Al8G7CqwoJOsW7Kddns=\
|
||||||
|
""";
|
||||||
|
|
||||||
private LicenseChecker licenseChecker;
|
private LicenseChecker licenseChecker;
|
||||||
|
|
||||||
|
|||||||
@@ -44,4 +44,9 @@ class MapKeychainAccess implements KeychainAccessProvider {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLocked() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-8
@@ -5,26 +5,34 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.common.settings;
|
package org.cryptomator.common.settings;
|
||||||
|
|
||||||
|
import org.cryptomator.common.Environment;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class SettingsJsonAdapterTest {
|
public class SettingsJsonAdapterTest {
|
||||||
|
|
||||||
private final SettingsJsonAdapter adapter = new SettingsJsonAdapter();
|
private final Environment env = Mockito.mock(Environment.class);
|
||||||
|
private final SettingsJsonAdapter adapter = new SettingsJsonAdapter(env);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDeserialize() throws IOException {
|
public void testDeserialize() throws IOException {
|
||||||
String vault1Json = "{\"id\": \"1\", \"path\": \"/vault1\", \"mountName\": \"vault1\", \"winDriveLetter\": \"X\"}";
|
String json = """
|
||||||
String vault2Json = "{\"id\": \"2\", \"path\": \"/vault2\", \"mountName\": \"vault2\", \"winDriveLetter\": \"Y\"}";
|
{
|
||||||
String json = "{\"directories\": [" + vault1Json + "," + vault2Json + "]," //
|
"directories": [
|
||||||
+ "\"checkForUpdatesEnabled\": true,"//
|
{"id": "1", "path": "/vault1", "mountName": "vault1", "winDriveLetter": "X"},
|
||||||
+ "\"port\": 8080,"//
|
{"id": "2", "path": "/vault2", "mountName": "vault2", "winDriveLetter": "Y"}
|
||||||
+ "\"numTrayNotifications\": 42,"//
|
],
|
||||||
+ "\"preferredVolumeImpl\": \"FUSE\"}";
|
"checkForUpdatesEnabled": true,
|
||||||
|
"port": 8080,
|
||||||
|
"numTrayNotifications": 42,
|
||||||
|
"preferredVolumeImpl": "FUSE"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
Settings settings = adapter.fromJson(json);
|
Settings settings = adapter.fromJson(json);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.common.settings;
|
package org.cryptomator.common.settings;
|
||||||
|
|
||||||
|
import org.cryptomator.common.Environment;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
@@ -14,8 +15,10 @@ public class SettingsTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAutoSave() {
|
public void testAutoSave() {
|
||||||
|
Environment env = Mockito.mock(Environment.class);
|
||||||
@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
|
@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
|
||||||
Settings settings = new Settings();
|
|
||||||
|
Settings settings = new Settings(env);
|
||||||
settings.setSaveCmd(changeListener);
|
settings.setSaveCmd(changeListener);
|
||||||
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
||||||
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
|
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class VaultModuleTest {
|
|||||||
|
|
||||||
StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);
|
StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);
|
||||||
|
|
||||||
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ovolname=TEST"));
|
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ovolname=\"TEST\""));
|
||||||
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ordonly"));
|
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ordonly"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.5.10</version>
|
<version>1.6.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>launcher</artifactId>
|
<artifactId>launcher</artifactId>
|
||||||
<name>Cryptomator Launcher</name>
|
<name>Cryptomator Launcher</name>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class FileOpenRequestHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void openFiles(OpenFilesEvent evt) {
|
private void openFiles(OpenFilesEvent evt) {
|
||||||
Collection<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath).collect(Collectors.toList());
|
Collection<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath).toList();
|
||||||
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
|
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
|
||||||
tryToEnqueueFileOpenRequest(launchEvent);
|
tryToEnqueueFileOpenRequest(launchEvent);
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ class FileOpenRequestHandler {
|
|||||||
LOG.trace("Argument not a valid path: {}", str);
|
LOG.trace("Argument not a valid path: {}", str);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).filter(Objects::nonNull).collect(Collectors.toList());
|
}).filter(Objects::nonNull).toList();
|
||||||
if (!pathsToOpen.isEmpty()) {
|
if (!pathsToOpen.isEmpty()) {
|
||||||
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
|
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
|
||||||
tryToEnqueueFileOpenRequest(launchEvent);
|
tryToEnqueueFileOpenRequest(launchEvent);
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ public class LoggerModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
static LoggerContext provideLoggerContext() {
|
static LoggerContext provideLoggerContext() {
|
||||||
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
|
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
|
||||||
if (loggerFactory instanceof LoggerContext) {
|
if (loggerFactory instanceof LoggerContext context) {
|
||||||
return (LoggerContext) loggerFactory;
|
return context;
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("SLF4J not bound to Logback.");
|
throw new IllegalStateException("SLF4J not bound to Logback.");
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-23
@@ -3,7 +3,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.5.10</version>
|
<version>1.6.0</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<name>Cryptomator</name>
|
<name>Cryptomator</name>
|
||||||
|
|
||||||
@@ -22,41 +22,35 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<project.jdk.version>16</project.jdk.version>
|
||||||
|
|
||||||
<!-- cryptomator dependencies -->
|
<!-- cryptomator dependencies -->
|
||||||
<cryptomator.cryptofs.version>1.9.13</cryptomator.cryptofs.version>
|
<cryptomator.cryptofs.version>2.1.0-beta5</cryptomator.cryptofs.version>
|
||||||
<cryptomator.integrations.version>0.1.6</cryptomator.integrations.version>
|
<cryptomator.integrations.version>1.0.0-beta2</cryptomator.integrations.version>
|
||||||
<cryptomator.integrations.win.version>0.1.0-beta1</cryptomator.integrations.win.version>
|
<cryptomator.integrations.win.version>1.0.0-beta2</cryptomator.integrations.win.version>
|
||||||
<cryptomator.integrations.mac.version>0.1.0-beta3</cryptomator.integrations.mac.version>
|
<cryptomator.integrations.mac.version>1.0.0-beta2</cryptomator.integrations.mac.version>
|
||||||
<cryptomator.integrations.linux.version>0.1.0-beta2</cryptomator.integrations.linux.version>
|
<cryptomator.integrations.linux.version>1.0.0-beta1</cryptomator.integrations.linux.version>
|
||||||
<cryptomator.fuse.version>1.2.5</cryptomator.fuse.version>
|
<cryptomator.fuse.version>1.3.1</cryptomator.fuse.version>
|
||||||
<cryptomator.dokany.version>1.2.0</cryptomator.dokany.version>
|
<cryptomator.dokany.version>1.3.1</cryptomator.dokany.version>
|
||||||
<cryptomator.webdav.version>1.0.13</cryptomator.webdav.version>
|
<cryptomator.webdav.version>1.2.2</cryptomator.webdav.version>
|
||||||
|
|
||||||
<!-- 3rd party dependencies -->
|
<!-- 3rd party dependencies -->
|
||||||
<javafx.version>15</javafx.version>
|
<javafx.version>16</javafx.version>
|
||||||
<commons-lang3.version>3.11</commons-lang3.version>
|
<commons-lang3.version>3.11</commons-lang3.version>
|
||||||
<jwt.version>3.11.0</jwt.version>
|
<jwt.version>3.15.0</jwt.version>
|
||||||
<easybind.version>2.1.0</easybind.version>
|
<easybind.version>2.1.0</easybind.version>
|
||||||
<guava.version>30.0-jre</guava.version>
|
<guava.version>30.1.1-jre</guava.version>
|
||||||
<dagger.version>2.29.1</dagger.version>
|
<dagger.version>2.35.1</dagger.version>
|
||||||
<gson.version>2.8.6</gson.version>
|
<gson.version>2.8.6</gson.version>
|
||||||
<slf4j.version>1.7.30</slf4j.version>
|
<slf4j.version>1.7.30</slf4j.version>
|
||||||
<logback.version>1.2.3</logback.version>
|
<logback.version>1.2.3</logback.version>
|
||||||
|
|
||||||
<!-- test dependencies -->
|
<!-- test dependencies -->
|
||||||
<junit.jupiter.version>5.7.0</junit.jupiter.version>
|
<junit.jupiter.version>5.7.1</junit.jupiter.version>
|
||||||
<mockito.version>3.6.0</mockito.version>
|
<mockito.version>3.9.0</mockito.version>
|
||||||
<hamcrest.version>2.2</hamcrest.version>
|
<hamcrest.version>2.2</hamcrest.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>jcenter</id>
|
|
||||||
<url>https://jcenter.bintray.com</url>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- modules -->
|
<!-- modules -->
|
||||||
@@ -223,6 +217,7 @@
|
|||||||
<version>${javafx.version}</version>
|
<version>${javafx.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
@@ -325,6 +320,32 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</profile>
|
</profile>
|
||||||
|
<profile>
|
||||||
|
<id>dependency-check</id>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.owasp</groupId>
|
||||||
|
<artifactId>dependency-check-maven</artifactId>
|
||||||
|
<version>6.0.3</version>
|
||||||
|
<configuration>
|
||||||
|
<cveValidForHours>24</cveValidForHours>
|
||||||
|
<failBuildOnCVSS>0</failBuildOnCVSS>
|
||||||
|
<skipTestScope>true</skipTestScope>
|
||||||
|
<detail>true</detail>
|
||||||
|
<suppressionFile>suppression.xml</suppressionFile>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
</profiles>
|
</profiles>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -404,7 +425,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<configuration>
|
<configuration>
|
||||||
<release>14</release>
|
<release>${project.jdk.version}</release>
|
||||||
<annotationProcessorPaths>
|
<annotationProcessorPaths>
|
||||||
<path>
|
<path>
|
||||||
<groupId>com.google.dagger</groupId>
|
<groupId>com.google.dagger</groupId>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- This file lists false positives found by org.owasp:dependency-check-maven build plugin -->
|
||||||
|
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.2.xsd">
|
||||||
|
<suppress>
|
||||||
|
<notes><![CDATA[ Suppress known vulnerabilities in FUSE libraries for fuse-nio-adapter. For more info, see suppression.xml of https://github.com/cryptomator/fuse-nio-adapter ]]></notes>
|
||||||
|
<gav regex="true">^org\.cryptomator:fuse-nio-adapter:.*$</gav>
|
||||||
|
<cvssBelow>9</cvssBelow>
|
||||||
|
</suppress>
|
||||||
|
<suppress>
|
||||||
|
<notes><![CDATA[ Suppress known vulnerabilities in FUSE libraries for jnr-fuse (dependency of fuse-nio-adapter). ]]></notes>
|
||||||
|
<gav regex="true">^com\.github\.serceman:jnr-fuse:.*$</gav>
|
||||||
|
<cvssBelow>9</cvssBelow>
|
||||||
|
</suppress>
|
||||||
|
</suppressions>
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.5.10</version>
|
<version>1.6.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>ui</artifactId>
|
<artifactId>ui</artifactId>
|
||||||
<name>Cryptomator GUI</name>
|
<name>Cryptomator GUI</name>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import dagger.Provides;
|
|||||||
import dagger.multibindings.IntoMap;
|
import dagger.multibindings.IntoMap;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||||
import org.cryptomator.ui.common.FXMLLoaderFactory;
|
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.common.FxControllerKey;
|
import org.cryptomator.ui.common.FxControllerKey;
|
||||||
import org.cryptomator.ui.common.FxmlFile;
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||||
import org.cryptomator.ui.common.FxmlScene;
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
import org.cryptomator.ui.common.NewPasswordController;
|
import org.cryptomator.ui.common.NewPasswordController;
|
||||||
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
||||||
@@ -33,18 +33,11 @@ import java.util.ResourceBundle;
|
|||||||
@Module
|
@Module
|
||||||
public abstract class AddVaultModule {
|
public abstract class AddVaultModule {
|
||||||
|
|
||||||
@Provides
|
|
||||||
@AddVaultWizardScoped
|
|
||||||
@Named("newPassword")
|
|
||||||
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
|
|
||||||
return new SimpleObjectProperty<>("");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@AddVaultWizardWindow
|
@AddVaultWizardWindow
|
||||||
@AddVaultWizardScoped
|
@AddVaultWizardScoped
|
||||||
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||||
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
|
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -91,50 +84,50 @@ public abstract class AddVaultModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@FxmlScene(FxmlFile.ADDVAULT_WELCOME)
|
@FxmlScene(FxmlFile.ADDVAULT_WELCOME)
|
||||||
@AddVaultWizardScoped
|
@AddVaultWizardScoped
|
||||||
static Scene provideWelcomeScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
|
static Scene provideWelcomeScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_WELCOME.getRessourcePathString());
|
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_WELCOME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@FxmlScene(FxmlFile.ADDVAULT_EXISTING)
|
@FxmlScene(FxmlFile.ADDVAULT_EXISTING)
|
||||||
@AddVaultWizardScoped
|
@AddVaultWizardScoped
|
||||||
static Scene provideChooseExistingVaultScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
|
static Scene provideChooseExistingVaultScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_EXISTING.getRessourcePathString());
|
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_EXISTING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@FxmlScene(FxmlFile.ADDVAULT_NEW_NAME)
|
@FxmlScene(FxmlFile.ADDVAULT_NEW_NAME)
|
||||||
@AddVaultWizardScoped
|
@AddVaultWizardScoped
|
||||||
static Scene provideCreateNewVaultNameScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
|
static Scene provideCreateNewVaultNameScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_NAME.getRessourcePathString());
|
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION)
|
@FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION)
|
||||||
@AddVaultWizardScoped
|
@AddVaultWizardScoped
|
||||||
static Scene provideCreateNewVaultLocationScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
|
static Scene provideCreateNewVaultLocationScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_LOCATION.getRessourcePathString());
|
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_LOCATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD)
|
@FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD)
|
||||||
@AddVaultWizardScoped
|
@AddVaultWizardScoped
|
||||||
static Scene provideCreateNewVaultPasswordScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
|
static Scene provideCreateNewVaultPasswordScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_PASSWORD.getRessourcePathString());
|
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY)
|
@FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY)
|
||||||
@AddVaultWizardScoped
|
@AddVaultWizardScoped
|
||||||
static Scene provideCreateNewVaultRecoveryKeyScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
|
static Scene provideCreateNewVaultRecoveryKeyScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY.getRessourcePathString());
|
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@FxmlScene(FxmlFile.ADDVAULT_SUCCESS)
|
@FxmlScene(FxmlFile.ADDVAULT_SUCCESS)
|
||||||
@AddVaultWizardScoped
|
@AddVaultWizardScoped
|
||||||
static Scene provideCreateNewVaultSuccessScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
|
static Scene provideCreateNewVaultSuccessScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_SUCCESS.getRessourcePathString());
|
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------
|
// ------------------
|
||||||
@@ -167,8 +160,8 @@ public abstract class AddVaultModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FxControllerKey(NewPasswordController.class)
|
@FxControllerKey(NewPasswordController.class)
|
||||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
|
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||||
return new NewPasswordController(resourceBundle, strengthRater, password);
|
return new NewPasswordController(resourceBundle, strengthRater);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
|
|||||||
+1
-1
@@ -81,7 +81,7 @@ public class ChooseExistingVaultController implements FxController {
|
|||||||
Vault newVault = vaultListManager.add(vaultPath.get());
|
Vault newVault = vaultListManager.add(vaultPath.get());
|
||||||
vault.set(newVault);
|
vault.set(newVault);
|
||||||
window.setScene(successScene.get());
|
window.setScene(successScene.get());
|
||||||
} catch (NoSuchFileException e) {
|
} catch (IOException e) {
|
||||||
LOG.error("Failed to open existing vault.", e);
|
LOG.error("Failed to open existing vault.", e);
|
||||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-44
@@ -1,11 +1,10 @@
|
|||||||
package org.cryptomator.ui.addvaultwizard;
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
import com.tobiasdiez.easybind.EasyBind;
|
|
||||||
import dagger.Lazy;
|
import dagger.Lazy;
|
||||||
import org.cryptomator.ui.common.ErrorComponent;
|
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.common.FxmlFile;
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
import org.cryptomator.ui.common.FxmlScene;
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -16,21 +15,21 @@ import javafx.beans.binding.BooleanBinding;
|
|||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.Node;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.RadioButton;
|
import javafx.scene.control.RadioButton;
|
||||||
import javafx.scene.control.Toggle;
|
import javafx.scene.control.Toggle;
|
||||||
import javafx.scene.control.ToggleGroup;
|
import javafx.scene.control.ToggleGroup;
|
||||||
import javafx.stage.DirectoryChooser;
|
import javafx.stage.DirectoryChooser;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.NoSuchFileException;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
@@ -44,55 +43,74 @@ public class CreateNewVaultLocationController implements FxController {
|
|||||||
private final Stage window;
|
private final Stage window;
|
||||||
private final Lazy<Scene> chooseNameScene;
|
private final Lazy<Scene> chooseNameScene;
|
||||||
private final Lazy<Scene> choosePasswordScene;
|
private final Lazy<Scene> choosePasswordScene;
|
||||||
private final ErrorComponent.Builder errorComponent;
|
|
||||||
private final LocationPresets locationPresets;
|
private final LocationPresets locationPresets;
|
||||||
private final ObjectProperty<Path> vaultPath;
|
private final ObjectProperty<Path> vaultPath;
|
||||||
private final StringProperty vaultName;
|
private final StringProperty vaultName;
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
private final BooleanBinding validVaultPath;
|
private final BooleanBinding validVaultPath;
|
||||||
private final BooleanProperty usePresetPath;
|
private final BooleanProperty usePresetPath;
|
||||||
private final StringProperty warningText;
|
private final StringProperty statusText;
|
||||||
|
private final ObjectProperty<Node> statusGraphic;
|
||||||
|
|
||||||
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
|
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
|
||||||
|
|
||||||
|
//FXML
|
||||||
public ToggleGroup predefinedLocationToggler;
|
public ToggleGroup predefinedLocationToggler;
|
||||||
public RadioButton iclouddriveRadioButton;
|
public RadioButton iclouddriveRadioButton;
|
||||||
public RadioButton dropboxRadioButton;
|
public RadioButton dropboxRadioButton;
|
||||||
public RadioButton gdriveRadioButton;
|
public RadioButton gdriveRadioButton;
|
||||||
public RadioButton onedriveRadioButton;
|
public RadioButton onedriveRadioButton;
|
||||||
|
public RadioButton megaRadioButton;
|
||||||
|
public RadioButton pcloudRadioButton;
|
||||||
public RadioButton customRadioButton;
|
public RadioButton customRadioButton;
|
||||||
|
public Label vaultPathStatus;
|
||||||
|
public FontAwesome5IconView goodLocation;
|
||||||
|
public FontAwesome5IconView badLocation;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ErrorComponent.Builder errorComponent, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
|
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.chooseNameScene = chooseNameScene;
|
this.chooseNameScene = chooseNameScene;
|
||||||
this.choosePasswordScene = choosePasswordScene;
|
this.choosePasswordScene = choosePasswordScene;
|
||||||
this.errorComponent = errorComponent;
|
|
||||||
this.locationPresets = locationPresets;
|
this.locationPresets = locationPresets;
|
||||||
this.vaultPath = vaultPath;
|
this.vaultPath = vaultPath;
|
||||||
this.vaultName = vaultName;
|
this.vaultName = vaultName;
|
||||||
this.resourceBundle = resourceBundle;
|
this.resourceBundle = resourceBundle;
|
||||||
this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath);
|
this.validVaultPath = Bindings.createBooleanBinding(this::validateVaultPathAndSetStatus, this.vaultPath);
|
||||||
this.usePresetPath = new SimpleBooleanProperty();
|
this.usePresetPath = new SimpleBooleanProperty();
|
||||||
this.warningText = new SimpleStringProperty();
|
this.statusText = new SimpleStringProperty();
|
||||||
|
this.statusGraphic = new SimpleObjectProperty<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidVaultPath() {
|
private boolean validateVaultPathAndSetStatus() {
|
||||||
return vaultPath.get() != null && Files.notExists(vaultPath.get());
|
final Path p = vaultPath.get();
|
||||||
|
if (p == null) {
|
||||||
|
statusText.set("Error: Path is NULL.");
|
||||||
|
statusGraphic.set(badLocation);
|
||||||
|
return false;
|
||||||
|
} else if (!Files.exists(p.getParent())) {
|
||||||
|
statusText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
|
||||||
|
statusGraphic.set(badLocation);
|
||||||
|
return false;
|
||||||
|
} else if (!Files.isWritable(p.getParent())) {
|
||||||
|
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsNotWritable"));
|
||||||
|
statusGraphic.set(badLocation);
|
||||||
|
return false;
|
||||||
|
} else if (!Files.notExists(p)) {
|
||||||
|
statusText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
||||||
|
statusGraphic.set(badLocation);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsOk"));
|
||||||
|
statusGraphic.set(goodLocation);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
|
predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
|
||||||
usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
|
usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
|
||||||
EasyBind.subscribe(vaultPath, this::vaultPathDidChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void vaultPathDidChange(Path newValue) {
|
|
||||||
if (newValue != null && !Files.notExists(newValue)) {
|
|
||||||
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
|
||||||
} else {
|
|
||||||
warningText.set(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
|
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
|
||||||
@@ -104,6 +122,10 @@ public class CreateNewVaultLocationController implements FxController {
|
|||||||
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
|
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
|
||||||
} else if (onedriveRadioButton.equals(newValue)) {
|
} else if (onedriveRadioButton.equals(newValue)) {
|
||||||
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
|
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
|
||||||
|
} else if (megaRadioButton.equals(newValue)) {
|
||||||
|
vaultPath.set(locationPresets.getMegaLocation().resolve(vaultName.get()));
|
||||||
|
} else if (pcloudRadioButton.equals(newValue)) {
|
||||||
|
vaultPath.set(locationPresets.getPcloudLocation().resolve(vaultName.get()));
|
||||||
} else if (customRadioButton.equals(newValue)) {
|
} else if (customRadioButton.equals(newValue)) {
|
||||||
vaultPath.set(customVaultPath.resolve(vaultName.get()));
|
vaultPath.set(customVaultPath.resolve(vaultName.get()));
|
||||||
}
|
}
|
||||||
@@ -116,21 +138,10 @@ public class CreateNewVaultLocationController implements FxController {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void next() {
|
public void next() {
|
||||||
try {
|
if (validateVaultPathAndSetStatus()) {
|
||||||
// check if we have write access AND the vaultPath doesn't already exist:
|
|
||||||
assert Files.isDirectory(vaultPath.get().getParent());
|
|
||||||
Path createdDir = Files.createDirectory(vaultPath.get());
|
|
||||||
Files.delete(createdDir); // assert: dir exists and is empty
|
|
||||||
window.setScene(choosePasswordScene.get());
|
window.setScene(choosePasswordScene.get());
|
||||||
} catch (FileAlreadyExistsException e) {
|
} else {
|
||||||
LOG.warn("Can not use already existing vault path {}", vaultPath.get());
|
validVaultPath.invalidate();
|
||||||
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
|
||||||
} catch (NoSuchFileException e) {
|
|
||||||
LOG.warn("At least one path component does not exist of path {}", vaultPath.get());
|
|
||||||
warningText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.error("Failed to create and delete directory at chosen vault path.", e);
|
|
||||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,19 +191,27 @@ public class CreateNewVaultLocationController implements FxController {
|
|||||||
return usePresetPath.get();
|
return usePresetPath.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty warningTextProperty() {
|
public BooleanBinding anyRadioButtonSelectedProperty() {
|
||||||
return warningText;
|
return predefinedLocationToggler.selectedToggleProperty().isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getWarningText() {
|
public boolean isAnyRadioButtonSelected() {
|
||||||
return warningText.get();
|
return anyRadioButtonSelectedProperty().get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanBinding showWarningProperty() {
|
public StringProperty statusTextProperty() {
|
||||||
return warningText.isNotEmpty();
|
return statusText;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isShowWarning() {
|
public String getStatusText() {
|
||||||
return showWarningProperty().get();
|
return statusText.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<Node> statusGraphicProperty() {
|
||||||
|
return statusGraphic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node getStatusGraphic() {
|
||||||
|
return statusGraphic.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-23
@@ -5,11 +5,18 @@ import org.cryptomator.common.vaults.Vault;
|
|||||||
import org.cryptomator.common.vaults.VaultListManager;
|
import org.cryptomator.common.vaults.VaultListManager;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||||
|
import org.cryptomator.cryptofs.VaultCipherCombo;
|
||||||
|
import org.cryptomator.cryptolib.api.CryptoException;
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||||
|
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||||
import org.cryptomator.ui.common.ErrorComponent;
|
import org.cryptomator.ui.common.ErrorComponent;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.common.FxmlFile;
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
import org.cryptomator.ui.common.FxmlScene;
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.cryptomator.ui.common.NewPasswordController;
|
||||||
import org.cryptomator.ui.common.Tasks;
|
import org.cryptomator.ui.common.Tasks;
|
||||||
|
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
|
||||||
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
|
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -17,7 +24,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
|
||||||
import javafx.beans.binding.ObjectBinding;
|
import javafx.beans.binding.ObjectBinding;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
@@ -31,13 +37,13 @@ import javafx.scene.control.ToggleGroup;
|
|||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
|
import java.net.URI;
|
||||||
import java.nio.channels.WritableByteChannel;
|
import java.nio.channels.WritableByteChannel;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.NoSuchFileException;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.Collections;
|
import java.security.SecureRandom;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
@@ -48,6 +54,7 @@ import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
|||||||
public class CreateNewVaultPasswordController implements FxController {
|
public class CreateNewVaultPasswordController implements FxController {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class);
|
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class);
|
||||||
|
private static final URI DEFAULT_KEY_ID = URI.create(MasterkeyFileLoadingStrategy.SCHEME + ":" + MASTERKEY_FILENAME); // TODO better place?
|
||||||
|
|
||||||
private final Stage window;
|
private final Stage window;
|
||||||
private final Lazy<Scene> chooseLocationScene;
|
private final Lazy<Scene> chooseLocationScene;
|
||||||
@@ -62,8 +69,9 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
private final StringProperty recoveryKeyProperty;
|
private final StringProperty recoveryKeyProperty;
|
||||||
private final VaultListManager vaultListManager;
|
private final VaultListManager vaultListManager;
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
private final ObjectProperty<CharSequence> password;
|
|
||||||
private final ReadmeGenerator readmeGenerator;
|
private final ReadmeGenerator readmeGenerator;
|
||||||
|
private final SecureRandom csprng;
|
||||||
|
private final MasterkeyFileAccess masterkeyFileAccess;
|
||||||
private final BooleanProperty processing;
|
private final BooleanProperty processing;
|
||||||
private final BooleanProperty readyToCreateVault;
|
private final BooleanProperty readyToCreateVault;
|
||||||
private final ObjectBinding<ContentDisplay> createVaultButtonState;
|
private final ObjectBinding<ContentDisplay> createVaultButtonState;
|
||||||
@@ -71,9 +79,10 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
public ToggleGroup recoveryKeyChoice;
|
public ToggleGroup recoveryKeyChoice;
|
||||||
public Toggle showRecoveryKey;
|
public Toggle showRecoveryKey;
|
||||||
public Toggle skipRecoveryKey;
|
public Toggle skipRecoveryKey;
|
||||||
|
public NewPasswordController newPasswordSceneController;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty<CharSequence> password, ReadmeGenerator readmeGenerator) {
|
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, ReadmeGenerator readmeGenerator, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.chooseLocationScene = chooseLocationScene;
|
this.chooseLocationScene = chooseLocationScene;
|
||||||
this.recoveryKeyScene = recoveryKeyScene;
|
this.recoveryKeyScene = recoveryKeyScene;
|
||||||
@@ -87,8 +96,9 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
this.recoveryKeyProperty = recoveryKey;
|
this.recoveryKeyProperty = recoveryKey;
|
||||||
this.vaultListManager = vaultListManager;
|
this.vaultListManager = vaultListManager;
|
||||||
this.resourceBundle = resourceBundle;
|
this.resourceBundle = resourceBundle;
|
||||||
this.password = password;
|
|
||||||
this.readmeGenerator = readmeGenerator;
|
this.readmeGenerator = readmeGenerator;
|
||||||
|
this.csprng = csprng;
|
||||||
|
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||||
this.processing = new SimpleBooleanProperty();
|
this.processing = new SimpleBooleanProperty();
|
||||||
this.readyToCreateVault = new SimpleBooleanProperty();
|
this.readyToCreateVault = new SimpleBooleanProperty();
|
||||||
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
|
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
|
||||||
@@ -96,8 +106,11 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
BooleanBinding isValidNewPassword = Bindings.createBooleanBinding(() -> password.get() != null && password.get().length() > 0, password);
|
readyToCreateVault.bind(newPasswordSceneController.passwordsMatchAndSufficientProperty().and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
|
||||||
readyToCreateVault.bind(isValidNewPassword.and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
|
window.setOnHiding(event -> {
|
||||||
|
newPasswordSceneController.passwordField.wipe();
|
||||||
|
newPasswordSceneController.reenterField.wipe();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -130,8 +143,8 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
Path pathToVault = vaultPathProperty.get();
|
Path pathToVault = vaultPathProperty.get();
|
||||||
processing.set(true);
|
processing.set(true);
|
||||||
Tasks.create(() -> {
|
Tasks.create(() -> {
|
||||||
initializeVault(pathToVault, password.get());
|
initializeVault(pathToVault);
|
||||||
return recoveryKeyFactory.createRecoveryKey(pathToVault, password.get());
|
return recoveryKeyFactory.createRecoveryKey(pathToVault, newPasswordSceneController.passwordField.getCharacters());
|
||||||
}).onSuccess(recoveryKey -> {
|
}).onSuccess(recoveryKey -> {
|
||||||
initializationSucceeded(pathToVault);
|
initializationSucceeded(pathToVault);
|
||||||
recoveryKeyProperty.set(recoveryKey);
|
recoveryKeyProperty.set(recoveryKey);
|
||||||
@@ -148,7 +161,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
Path pathToVault = vaultPathProperty.get();
|
Path pathToVault = vaultPathProperty.get();
|
||||||
processing.set(true);
|
processing.set(true);
|
||||||
Tasks.create(() -> {
|
Tasks.create(() -> {
|
||||||
initializeVault(pathToVault, password.get());
|
initializeVault(pathToVault);
|
||||||
}).onSuccess(() -> {
|
}).onSuccess(() -> {
|
||||||
initializationSucceeded(pathToVault);
|
initializationSucceeded(pathToVault);
|
||||||
window.setScene(successScene.get());
|
window.setScene(successScene.get());
|
||||||
@@ -160,24 +173,35 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
}).runOnce(executor);
|
}).runOnce(executor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeVault(Path path, CharSequence passphrase) throws IOException {
|
private void initializeVault(Path path) throws IOException {
|
||||||
CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase);
|
// 1. write masterkey:
|
||||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME);
|
||||||
.withPassphrase(passphrase) //
|
try (Masterkey masterkey = Masterkey.generate(csprng)) {
|
||||||
.withFlags(Collections.emptySet()) //
|
masterkeyFileAccess.persist(masterkey, masterkeyFilePath, newPasswordSceneController.passwordField.getCharacters());
|
||||||
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
|
||||||
.build();
|
|
||||||
|
|
||||||
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
// 2. initialize vault:
|
||||||
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
try {
|
||||||
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
MasterkeyLoader loader = ignored -> masterkey.clone();
|
||||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build();
|
||||||
|
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
|
||||||
|
|
||||||
|
// 3. write vault-internal readme file:
|
||||||
|
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
||||||
|
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
||||||
|
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||||
|
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
||||||
|
}
|
||||||
|
} catch (CryptoException e) {
|
||||||
|
throw new IOException("Failed initialize vault.", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. write vault-external readme file:
|
||||||
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
|
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
|
||||||
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
|
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.info("Created vault at {}", path);
|
LOG.info("Created vault at {}", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +209,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
try {
|
try {
|
||||||
Vault newVault = vaultListManager.add(pathToVault);
|
Vault newVault = vaultListManager.add(pathToVault);
|
||||||
vaultProperty.set(newVault);
|
vaultProperty.set(newVault);
|
||||||
} catch (NoSuchFileException e) {
|
} catch (IOException e) {
|
||||||
throw new UncheckedIOException(e);
|
throw new UncheckedIOException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,21 @@ public class LocationPresets {
|
|||||||
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
|
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
|
||||||
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"};
|
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"};
|
||||||
private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"};
|
private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"};
|
||||||
|
private static final String[] MEGA_LOCATIONS = {"~/MEGA"};
|
||||||
|
private static final String[] PCLOUD_LOCATIONS = {"~/pCloudDrive"};
|
||||||
|
|
||||||
private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
|
private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
|
||||||
private final ReadOnlyObjectProperty<Path> dropboxLocation;
|
private final ReadOnlyObjectProperty<Path> dropboxLocation;
|
||||||
private final ReadOnlyObjectProperty<Path> gdriveLocation;
|
private final ReadOnlyObjectProperty<Path> gdriveLocation;
|
||||||
private final ReadOnlyObjectProperty<Path> onedriveLocation;
|
private final ReadOnlyObjectProperty<Path> onedriveLocation;
|
||||||
|
private final ReadOnlyObjectProperty<Path> megaLocation;
|
||||||
|
private final ReadOnlyObjectProperty<Path> pcloudLocation;
|
||||||
private final BooleanBinding foundIclouddrive;
|
private final BooleanBinding foundIclouddrive;
|
||||||
private final BooleanBinding foundDropbox;
|
private final BooleanBinding foundDropbox;
|
||||||
private final BooleanBinding foundGdrive;
|
private final BooleanBinding foundGdrive;
|
||||||
private final BooleanBinding foundOnedrive;
|
private final BooleanBinding foundOnedrive;
|
||||||
|
private final BooleanBinding foundMega;
|
||||||
|
private final BooleanBinding foundPcloud;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public LocationPresets() {
|
public LocationPresets() {
|
||||||
@@ -32,10 +38,14 @@ public class LocationPresets {
|
|||||||
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
|
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
|
||||||
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
|
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
|
||||||
this.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_LOCATIONS));
|
this.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_LOCATIONS));
|
||||||
|
this.megaLocation = new SimpleObjectProperty<>(existingWritablePath(MEGA_LOCATIONS));
|
||||||
|
this.pcloudLocation = new SimpleObjectProperty<>(existingWritablePath(PCLOUD_LOCATIONS));
|
||||||
this.foundIclouddrive = iclouddriveLocation.isNotNull();
|
this.foundIclouddrive = iclouddriveLocation.isNotNull();
|
||||||
this.foundDropbox = dropboxLocation.isNotNull();
|
this.foundDropbox = dropboxLocation.isNotNull();
|
||||||
this.foundGdrive = gdriveLocation.isNotNull();
|
this.foundGdrive = gdriveLocation.isNotNull();
|
||||||
this.foundOnedrive = onedriveLocation.isNotNull();
|
this.foundOnedrive = onedriveLocation.isNotNull();
|
||||||
|
this.foundMega = megaLocation.isNotNull();
|
||||||
|
this.foundPcloud = pcloudLocation.isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Path existingWritablePath(String... candidates) {
|
private static Path existingWritablePath(String... candidates) {
|
||||||
@@ -122,4 +132,36 @@ public class LocationPresets {
|
|||||||
return foundOnedrive.get();
|
return foundOnedrive.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObjectProperty<Path> megaLocationProperty() {
|
||||||
|
return megaLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getMegaLocation() {
|
||||||
|
return megaLocation.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding foundMegaProperty() {
|
||||||
|
return foundMega;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFoundMega() {
|
||||||
|
return foundMega.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObjectProperty<Path> pcloudLocationProperty() {
|
||||||
|
return pcloudLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getPcloudLocation() {
|
||||||
|
return pcloudLocation.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding foundPcloudProperty() {
|
||||||
|
return foundPcloud;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFoundPcloud() {
|
||||||
|
return foundPcloud.get();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-17
@@ -2,29 +2,33 @@ package org.cryptomator.ui.changepassword;
|
|||||||
|
|
||||||
import org.cryptomator.common.keychain.KeychainManager;
|
import org.cryptomator.common.keychain.KeychainManager;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
|
||||||
|
import org.cryptomator.cryptolib.api.CryptoException;
|
||||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||||
|
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||||
import org.cryptomator.ui.common.Animations;
|
import org.cryptomator.ui.common.Animations;
|
||||||
import org.cryptomator.ui.common.ErrorComponent;
|
import org.cryptomator.ui.common.ErrorComponent;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.NewPasswordController;
|
||||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.binding.BooleanBinding;
|
import javafx.beans.binding.BooleanBinding;
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.CheckBox;
|
import javafx.scene.control.CheckBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.CharBuffer;
|
import java.nio.file.Files;
|
||||||
import java.util.Optional;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
|
||||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||||
|
|
||||||
@ChangePasswordScoped
|
@ChangePasswordScoped
|
||||||
@@ -34,29 +38,36 @@ public class ChangePasswordController implements FxController {
|
|||||||
|
|
||||||
private final Stage window;
|
private final Stage window;
|
||||||
private final Vault vault;
|
private final Vault vault;
|
||||||
private final ObjectProperty<CharSequence> newPassword;
|
|
||||||
private final ErrorComponent.Builder errorComponent;
|
private final ErrorComponent.Builder errorComponent;
|
||||||
private final KeychainManager keychain;
|
private final KeychainManager keychain;
|
||||||
|
private final SecureRandom csprng;
|
||||||
|
private final MasterkeyFileAccess masterkeyFileAccess;
|
||||||
|
|
||||||
public NiceSecurePasswordField oldPasswordField;
|
public NiceSecurePasswordField oldPasswordField;
|
||||||
public CheckBox finalConfirmationCheckbox;
|
public CheckBox finalConfirmationCheckbox;
|
||||||
public Button finishButton;
|
public Button finishButton;
|
||||||
|
public NewPasswordController newPasswordController;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain) {
|
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.newPassword = newPassword;
|
|
||||||
this.errorComponent = errorComponent;
|
this.errorComponent = errorComponent;
|
||||||
this.keychain = keychain;
|
this.keychain = keychain;
|
||||||
|
this.csprng = csprng;
|
||||||
|
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not();
|
BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not();
|
||||||
BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty();
|
BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty();
|
||||||
BooleanBinding newPasswordInvalid = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword);
|
finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordController.passwordsMatchAndSufficientProperty().not()));
|
||||||
finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordInvalid));
|
window.setOnHiding(event -> {
|
||||||
|
oldPasswordField.wipe();
|
||||||
|
newPasswordController.passwordField.wipe();
|
||||||
|
newPasswordController.reenterField.wipe();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -67,24 +78,31 @@ public class ChangePasswordController implements FxController {
|
|||||||
@FXML
|
@FXML
|
||||||
public void finish() {
|
public void finish() {
|
||||||
try {
|
try {
|
||||||
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
|
CharSequence oldPassphrase = oldPasswordField.getCharacters();
|
||||||
|
CharSequence newPassphrase = newPasswordController.passwordField.getCharacters();
|
||||||
|
Path masterkeyPath = vault.getPath().resolve(MASTERKEY_FILENAME);
|
||||||
|
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
|
||||||
|
byte[] newMasterkeyBytes = masterkeyFileAccess.changePassphrase(oldMasterkeyBytes, oldPassphrase, newPassphrase);
|
||||||
|
Path backupKeyPath = vault.getPath().resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||||
|
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
Files.write(masterkeyPath, newMasterkeyBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
|
||||||
LOG.info("Successfully changed password for {}", vault.getDisplayName());
|
LOG.info("Successfully changed password for {}", vault.getDisplayName());
|
||||||
window.close();
|
|
||||||
updatePasswordInSystemkeychain();
|
updatePasswordInSystemkeychain();
|
||||||
} catch (IOException e) {
|
window.close();
|
||||||
LOG.error("IO error occured during password change. Unable to perform operation.", e);
|
|
||||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
|
||||||
} catch (InvalidPassphraseException e) {
|
} catch (InvalidPassphraseException e) {
|
||||||
Animations.createShakeWindowAnimation(window).play();
|
Animations.createShakeWindowAnimation(window).play();
|
||||||
oldPasswordField.selectAll();
|
oldPasswordField.selectAll();
|
||||||
oldPasswordField.requestFocus();
|
oldPasswordField.requestFocus();
|
||||||
|
} catch (IOException | CryptoException e) {
|
||||||
|
LOG.error("Password change failed. Unable to perform operation.", e);
|
||||||
|
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePasswordInSystemkeychain() {
|
private void updatePasswordInSystemkeychain() {
|
||||||
if (keychain.isSupported()) {
|
if (keychain.isSupported()) {
|
||||||
try {
|
try {
|
||||||
keychain.changePassphrase(vault.getId(), CharBuffer.wrap(newPassword.get()));
|
keychain.changePassphrase(vault.getId(), newPasswordController.passwordField.getCharacters());
|
||||||
LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayName());
|
LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayName());
|
||||||
} catch (KeychainAccessException e) {
|
} catch (KeychainAccessException e) {
|
||||||
LOG.error("Failed to update password in system keychain.", e);
|
LOG.error("Failed to update password in system keychain.", e);
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import dagger.Module;
|
|||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import dagger.multibindings.IntoMap;
|
import dagger.multibindings.IntoMap;
|
||||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||||
import org.cryptomator.ui.common.FXMLLoaderFactory;
|
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.common.FxControllerKey;
|
import org.cryptomator.ui.common.FxControllerKey;
|
||||||
import org.cryptomator.ui.common.FxmlFile;
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||||
import org.cryptomator.ui.common.FxmlScene;
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
import org.cryptomator.ui.common.NewPasswordController;
|
import org.cryptomator.ui.common.NewPasswordController;
|
||||||
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
||||||
@@ -16,8 +16,6 @@ import org.cryptomator.ui.common.StageFactory;
|
|||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
@@ -27,18 +25,11 @@ import java.util.ResourceBundle;
|
|||||||
@Module
|
@Module
|
||||||
abstract class ChangePasswordModule {
|
abstract class ChangePasswordModule {
|
||||||
|
|
||||||
@Provides
|
|
||||||
@ChangePasswordScoped
|
|
||||||
@Named("newPassword")
|
|
||||||
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
|
|
||||||
return new SimpleObjectProperty<>("");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ChangePasswordWindow
|
@ChangePasswordWindow
|
||||||
@ChangePasswordScoped
|
@ChangePasswordScoped
|
||||||
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||||
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
|
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -56,8 +47,8 @@ abstract class ChangePasswordModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@FxmlScene(FxmlFile.CHANGEPASSWORD)
|
@FxmlScene(FxmlFile.CHANGEPASSWORD)
|
||||||
@ChangePasswordScoped
|
@ChangePasswordScoped
|
||||||
static Scene provideUnlockScene(@ChangePasswordWindow FXMLLoaderFactory fxmlLoaders) {
|
static Scene provideUnlockScene(@ChangePasswordWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
return fxmlLoaders.createScene("/fxml/changepassword.fxml");
|
return fxmlLoaders.createScene(FxmlFile.CHANGEPASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -71,8 +62,8 @@ abstract class ChangePasswordModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FxControllerKey(NewPasswordController.class)
|
@FxControllerKey(NewPasswordController.class)
|
||||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
|
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||||
return new NewPasswordController(resourceBundle, strengthRater, password);
|
return new NewPasswordController(resourceBundle, strengthRater);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ public class DefaultSceneFactory implements Function<Parent, Scene> {
|
|||||||
protected void configureScene(Scene scene) {
|
protected void configureScene(Scene scene) {
|
||||||
scene.windowProperty().addListener(observable -> {
|
scene.windowProperty().addListener(observable -> {
|
||||||
Window window = scene.getWindow();
|
Window window = scene.getWindow();
|
||||||
if (window instanceof Stage) {
|
if (window instanceof Stage s) {
|
||||||
setupDefaultAccelerators(scene, (Stage) window);
|
setupDefaultAccelerators(scene, s);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import java.util.ResourceBundle;
|
|||||||
abstract class ErrorModule {
|
abstract class ErrorModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||||
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
|
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -38,8 +38,8 @@ abstract class ErrorModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@FxmlScene(FxmlFile.ERROR)
|
@FxmlScene(FxmlFile.ERROR)
|
||||||
static Scene provideErrorScene(FXMLLoaderFactory fxmlLoaders) {
|
static Scene provideErrorScene(FxmlLoaderFactory fxmlLoaders) {
|
||||||
return fxmlLoaders.createScene(FxmlFile.ERROR.getRessourcePathString());
|
return fxmlLoaders.createScene(FxmlFile.ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ public enum FxmlFile {
|
|||||||
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
|
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
|
||||||
ERROR("/fxml/error.fxml"), //
|
ERROR("/fxml/error.fxml"), //
|
||||||
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
|
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
|
||||||
|
HEALTH_START("/fxml/health_start.fxml"), //
|
||||||
|
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
|
||||||
|
LOCK_FORCED("/fxml/lock_forced.fxml"), //
|
||||||
|
LOCK_FAILED("/fxml/lock_failed.fxml"), //
|
||||||
MAIN_WINDOW("/fxml/main_window.fxml"), //
|
MAIN_WINDOW("/fxml/main_window.fxml"), //
|
||||||
MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), //
|
MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), //
|
||||||
MIGRATION_IMPOSSIBLE("/fxml/migration_impossible.fxml"),
|
MIGRATION_IMPOSSIBLE("/fxml/migration_impossible.fxml"),
|
||||||
@@ -24,8 +28,9 @@ public enum FxmlFile {
|
|||||||
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
|
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
|
||||||
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
|
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
|
||||||
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
|
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
|
||||||
UNLOCK("/fxml/unlock.fxml"),
|
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
|
||||||
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
|
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
|
||||||
|
UNLOCK_SELECT_MASTERKEYFILE("/fxml/unlock_select_masterkeyfile.fxml"), //
|
||||||
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
|
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
|
||||||
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
|
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
|
||||||
VAULT_STATISTICS("/fxml/stats.fxml"), //
|
VAULT_STATISTICS("/fxml/stats.fxml"), //
|
||||||
@@ -37,7 +42,7 @@ public enum FxmlFile {
|
|||||||
this.ressourcePathString = ressourcePathString;
|
this.ressourcePathString = ressourcePathString;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRessourcePathString() {
|
String getRessourcePathString() {
|
||||||
return ressourcePathString;
|
return ressourcePathString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -11,13 +11,13 @@ import java.util.Map;
|
|||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class FXMLLoaderFactory {
|
public class FxmlLoaderFactory {
|
||||||
|
|
||||||
private final Map<Class<? extends FxController>, Provider<FxController>> controllerFactories;
|
private final Map<Class<? extends FxController>, Provider<FxController>> controllerFactories;
|
||||||
private final Function<Parent, Scene> sceneFactory;
|
private final Function<Parent, Scene> sceneFactory;
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
|
|
||||||
public FXMLLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> controllerFactories, Function<Parent, Scene> sceneFactory, ResourceBundle resourceBundle) {
|
public FxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> controllerFactories, Function<Parent, Scene> sceneFactory, ResourceBundle resourceBundle) {
|
||||||
this.controllerFactories = controllerFactories;
|
this.controllerFactories = controllerFactories;
|
||||||
this.sceneFactory = sceneFactory;
|
this.sceneFactory = sceneFactory;
|
||||||
this.resourceBundle = resourceBundle;
|
this.resourceBundle = resourceBundle;
|
||||||
@@ -48,13 +48,17 @@ public class FXMLLoaderFactory {
|
|||||||
return loader;
|
return loader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Scene createScene(FxmlFile fxmlFile) {
|
||||||
|
return createScene(fxmlFile.getRessourcePathString());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link #load(String) Loads} the FXML file and creates a new Scene containing the loaded ui.
|
* {@link #load(String) Loads} the FXML file and creates a new Scene containing the loaded ui.
|
||||||
*
|
*
|
||||||
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
|
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
|
||||||
* @throws UncheckedIOException wrapping any IOException thrown by {@link #load(String)).
|
* @throws UncheckedIOException wrapping any IOException thrown by {@link #load(String)).
|
||||||
*/
|
*/
|
||||||
public Scene createScene(String fxmlResourceName) {
|
private Scene createScene(String fxmlResourceName) {
|
||||||
final FXMLLoader loader;
|
final FXMLLoader loader;
|
||||||
try {
|
try {
|
||||||
loader = load(fxmlResourceName);
|
loader = load(fxmlResourceName);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
import org.cryptomator.common.vaults.Volume;
|
||||||
|
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
@FxApplicationScoped
|
||||||
|
public class HostServiceRevealer implements Volume.Revealer {
|
||||||
|
|
||||||
|
private final Lazy<Application> application;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public HostServiceRevealer(Lazy<Application> application) {
|
||||||
|
this.application = application;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reveal(Path p) throws Volume.VolumeException {
|
||||||
|
application.get().getHostServices().showDocument(p.toUri().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ import javafx.beans.Observable;
|
|||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
import javafx.beans.binding.BooleanBinding;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
@@ -18,8 +19,8 @@ public class NewPasswordController implements FxController {
|
|||||||
|
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
private final PasswordStrengthUtil strengthRater;
|
private final PasswordStrengthUtil strengthRater;
|
||||||
private final ObjectProperty<CharSequence> password;
|
|
||||||
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1);
|
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1);
|
||||||
|
private final ReadOnlyBooleanWrapper passwordsMatchAndSufficient = new ReadOnlyBooleanWrapper();
|
||||||
|
|
||||||
public NiceSecurePasswordField passwordField;
|
public NiceSecurePasswordField passwordField;
|
||||||
public NiceSecurePasswordField reenterField;
|
public NiceSecurePasswordField reenterField;
|
||||||
@@ -31,10 +32,9 @@ public class NewPasswordController implements FxController {
|
|||||||
public FontAwesome5IconView passwordMatchCheckmark;
|
public FontAwesome5IconView passwordMatchCheckmark;
|
||||||
public FontAwesome5IconView passwordMatchCross;
|
public FontAwesome5IconView passwordMatchCross;
|
||||||
|
|
||||||
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ObjectProperty<CharSequence> password) {
|
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||||
this.resourceBundle = resourceBundle;
|
this.resourceBundle = resourceBundle;
|
||||||
this.strengthRater = strengthRater;
|
this.strengthRater = strengthRater;
|
||||||
this.password = password;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -44,7 +44,7 @@ public class NewPasswordController implements FxController {
|
|||||||
passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength));
|
passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength));
|
||||||
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
||||||
|
|
||||||
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::hasSamePasswordInBothFields, passwordField.textProperty(), reenterField.textProperty());
|
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::passwordFieldsMatch, passwordField.textProperty(), reenterField.textProperty());
|
||||||
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
|
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
|
||||||
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
|
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
|
||||||
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(passwordMatchCheckmark).otherwise(passwordMatchCross));
|
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(passwordMatchCheckmark).otherwise(passwordMatchCross));
|
||||||
@@ -54,6 +54,7 @@ public class NewPasswordController implements FxController {
|
|||||||
reenterField.textProperty().addListener(this::passwordsDidChange);
|
reenterField.textProperty().addListener(this::passwordsDidChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private FontAwesome5IconView getIconViewForPasswordStrengthLabel() {
|
private FontAwesome5IconView getIconViewForPasswordStrengthLabel() {
|
||||||
if (passwordField.getCharacters().length() == 0) {
|
if (passwordField.getCharacters().length() == 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -67,17 +68,19 @@ public class NewPasswordController implements FxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
|
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
|
||||||
if (hasSamePasswordInBothFields() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
|
if (passwordFieldsMatch() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
|
||||||
password.set(passwordField.getCharacters());
|
passwordsMatchAndSufficient.setValue(true);
|
||||||
} else {
|
|
||||||
password.set("");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasSamePasswordInBothFields() {
|
private boolean passwordFieldsMatch() {
|
||||||
return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0;
|
return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReadOnlyBooleanProperty passwordsMatchAndSufficientProperty() {
|
||||||
|
return passwordsMatchAndSufficient.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
/* Getter/Setter */
|
/* Getter/Setter */
|
||||||
|
|
||||||
public IntegerProperty passwordStrengthProperty() {
|
public IntegerProperty passwordStrengthProperty() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.cryptomator.ui.common;
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
|
import org.cryptomator.common.vaults.LockNotCompletedException;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
import org.cryptomator.common.vaults.VaultState;
|
import org.cryptomator.common.vaults.VaultState;
|
||||||
import org.cryptomator.common.vaults.Volume;
|
import org.cryptomator.common.vaults.Volume;
|
||||||
@@ -23,10 +24,12 @@ public class VaultService {
|
|||||||
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
|
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
|
||||||
|
|
||||||
private final ExecutorService executorService;
|
private final ExecutorService executorService;
|
||||||
|
private final HostServiceRevealer vaultRevealer;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public VaultService(ExecutorService executorService) {
|
public VaultService(ExecutorService executorService, HostServiceRevealer vaultRevealer) {
|
||||||
this.executorService = executorService;
|
this.executorService = executorService;
|
||||||
|
this.vaultRevealer = vaultRevealer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reveal(Vault vault) {
|
public void reveal(Vault vault) {
|
||||||
@@ -39,7 +42,7 @@ public class VaultService {
|
|||||||
* @param vault The vault to reveal
|
* @param vault The vault to reveal
|
||||||
*/
|
*/
|
||||||
public Task<Vault> createRevealTask(Vault vault) {
|
public Task<Vault> createRevealTask(Vault vault) {
|
||||||
Task<Vault> task = new RevealVaultTask(vault);
|
Task<Vault> task = new RevealVaultTask(vault, vaultRevealer);
|
||||||
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName()));
|
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName()));
|
||||||
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException()));
|
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException()));
|
||||||
return task;
|
return task;
|
||||||
@@ -65,6 +68,7 @@ public class VaultService {
|
|||||||
public Task<Vault> createLockTask(Vault vault, boolean forced) {
|
public Task<Vault> createLockTask(Vault vault, boolean forced) {
|
||||||
Task<Vault> task = new LockVaultTask(vault, forced);
|
Task<Vault> task = new LockVaultTask(vault, forced);
|
||||||
task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayName()));
|
task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayName()));
|
||||||
|
task.setOnFailed(evt -> LOG.info("Failed to lock {}.", vault.getDisplayName(), evt.getSource().getException()));
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,19 +102,22 @@ public class VaultService {
|
|||||||
private static class RevealVaultTask extends Task<Vault> {
|
private static class RevealVaultTask extends Task<Vault> {
|
||||||
|
|
||||||
private final Vault vault;
|
private final Vault vault;
|
||||||
|
private final Volume.Revealer revealer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param vault The vault to lock
|
* @param vault The vault to lock
|
||||||
|
* @param revealer The object to use to show the vault content to the user.
|
||||||
*/
|
*/
|
||||||
public RevealVaultTask(Vault vault) {
|
public RevealVaultTask(Vault vault, Volume.Revealer revealer) {
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
|
this.revealer = revealer;
|
||||||
|
|
||||||
setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException()));
|
setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Vault call() throws Volume.VolumeException {
|
protected Vault call() throws Volume.VolumeException {
|
||||||
vault.reveal();
|
vault.reveal(revealer);
|
||||||
return vault;
|
return vault;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,24 +176,29 @@ public class VaultService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Vault call() throws Volume.VolumeException {
|
protected Vault call() throws Volume.VolumeException, LockNotCompletedException {
|
||||||
vault.lock(forced);
|
vault.lock(forced);
|
||||||
return vault;
|
return vault;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void scheduled() {
|
protected void scheduled() {
|
||||||
vault.setState(VaultState.PROCESSING);
|
vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void succeeded() {
|
protected void succeeded() {
|
||||||
vault.setState(VaultState.LOCKED);
|
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void failed() {
|
protected void failed() {
|
||||||
vault.setState(VaultState.UNLOCKED);
|
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cancelled() {
|
||||||
|
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ package org.cryptomator.ui.controls;
|
|||||||
public enum FontAwesome5Icon {
|
public enum FontAwesome5Icon {
|
||||||
ANCHOR("\uF13D"), //
|
ANCHOR("\uF13D"), //
|
||||||
ARROW_UP("\uF062"), //
|
ARROW_UP("\uF062"), //
|
||||||
|
BAN("\uF05E"), //
|
||||||
BUG("\uF188"), //
|
BUG("\uF188"), //
|
||||||
CHECK("\uF00C"), //
|
CHECK("\uF00C"), //
|
||||||
|
CLOCK("\uF017"), //
|
||||||
COG("\uF013"), //
|
COG("\uF013"), //
|
||||||
COGS("\uF085"), //
|
COGS("\uF085"), //
|
||||||
COPY("\uF0C5"), //
|
COPY("\uF0C5"), //
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public class FormattedLabel extends Label {
|
|||||||
|
|
||||||
private final StringProperty format = new SimpleStringProperty("");
|
private final StringProperty format = new SimpleStringProperty("");
|
||||||
private final ObjectProperty<Object> arg1 = new SimpleObjectProperty<>();
|
private final ObjectProperty<Object> arg1 = new SimpleObjectProperty<>();
|
||||||
|
private final ObjectProperty<Object> arg2 = new SimpleObjectProperty<>();
|
||||||
// add arg2, arg3, ... on demand
|
// add arg2, arg3, ... on demand
|
||||||
|
|
||||||
public FormattedLabel() {
|
public FormattedLabel() {
|
||||||
@@ -19,11 +20,11 @@ public class FormattedLabel extends Label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected StringBinding createStringBinding() {
|
protected StringBinding createStringBinding() {
|
||||||
return Bindings.createStringBinding(this::updateText, format, arg1);
|
return Bindings.createStringBinding(this::updateText, format, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String updateText() {
|
private String updateText() {
|
||||||
return String.format(format.get(), arg1.get());
|
return String.format(format.get(), arg1.get(), arg2.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Observables */
|
/* Observables */
|
||||||
@@ -51,4 +52,16 @@ public class FormattedLabel extends Label {
|
|||||||
public void setArg1(Object arg1) {
|
public void setArg1(Object arg1) {
|
||||||
this.arg1.set(arg1);
|
this.arg1.set(arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<Object> arg2Property() {
|
||||||
|
return arg2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getArg2() {
|
||||||
|
return arg2.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArg2(Object arg2) {
|
||||||
|
this.arg2.set(arg2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ public class SecurePasswordField extends TextField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateCapsLocked() {
|
private void updateCapsLocked() {
|
||||||
// AWT code needed until https://bugs.openjdk.java.net/browse/JDK-8090882 is closed:
|
//TODO: fixed in JavaFX 17. AWT code needed until update (see https://bugs.openjdk.java.net/browse/JDK-8259680)
|
||||||
capsLocked.set(isFocused() && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK));
|
capsLocked.set(isFocused() && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import dagger.Module;
|
|||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import dagger.multibindings.IntoMap;
|
import dagger.multibindings.IntoMap;
|
||||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||||
import org.cryptomator.ui.common.FXMLLoaderFactory;
|
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
import org.cryptomator.ui.common.FxControllerKey;
|
import org.cryptomator.ui.common.FxControllerKey;
|
||||||
import org.cryptomator.ui.common.FxmlFile;
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
@@ -29,8 +29,8 @@ abstract class ForgetPasswordModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@ForgetPasswordWindow
|
@ForgetPasswordWindow
|
||||||
@ForgetPasswordScoped
|
@ForgetPasswordScoped
|
||||||
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||||
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
|
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -62,8 +62,8 @@ abstract class ForgetPasswordModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@FxmlScene(FxmlFile.FORGET_PASSWORD)
|
@FxmlScene(FxmlFile.FORGET_PASSWORD)
|
||||||
@ForgetPasswordScoped
|
@ForgetPasswordScoped
|
||||||
static Scene provideForgetPasswordScene(@ForgetPasswordWindow FXMLLoaderFactory fxmlLoaders) {
|
static Scene provideForgetPasswordScene(@ForgetPasswordWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
return fxmlLoaders.createScene("/fxml/forget_password.fxml");
|
return fxmlLoaders.createScene(FxmlFile.FORGET_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------
|
// ------------------
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package org.cryptomator.ui.fxapp;
|
package org.cryptomator.ui.fxapp;
|
||||||
|
|
||||||
import com.tobiasdiez.easybind.EasyBind;
|
|
||||||
import dagger.Lazy;
|
import dagger.Lazy;
|
||||||
import org.cryptomator.common.LicenseHolder;
|
import org.cryptomator.common.LicenseHolder;
|
||||||
import org.cryptomator.common.settings.Settings;
|
import org.cryptomator.common.settings.Settings;
|
||||||
import org.cryptomator.common.settings.UiTheme;
|
import org.cryptomator.common.settings.UiTheme;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.common.vaults.VaultState;
|
||||||
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
|
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
|
||||||
import org.cryptomator.integrations.uiappearance.Theme;
|
import org.cryptomator.integrations.uiappearance.Theme;
|
||||||
import org.cryptomator.integrations.uiappearance.UiAppearanceException;
|
import org.cryptomator.integrations.uiappearance.UiAppearanceException;
|
||||||
import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
|
import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
|
||||||
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
|
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
|
||||||
|
import org.cryptomator.ui.common.ErrorComponent;
|
||||||
import org.cryptomator.ui.common.VaultService;
|
import org.cryptomator.ui.common.VaultService;
|
||||||
|
import org.cryptomator.ui.lock.LockComponent;
|
||||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||||
import org.cryptomator.ui.preferences.PreferencesComponent;
|
import org.cryptomator.ui.preferences.PreferencesComponent;
|
||||||
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
||||||
@@ -27,10 +29,13 @@ 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.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.collections.ObservableSet;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.Window;
|
||||||
import java.awt.desktop.QuitResponse;
|
import java.awt.desktop.QuitResponse;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
|
|
||||||
@FxApplicationScoped
|
@FxApplicationScoped
|
||||||
public class FxApplication extends Application {
|
public class FxApplication extends Application {
|
||||||
@@ -40,34 +45,40 @@ public class FxApplication extends Application {
|
|||||||
private final Settings settings;
|
private final Settings settings;
|
||||||
private final Lazy<MainWindowComponent> mainWindow;
|
private final Lazy<MainWindowComponent> mainWindow;
|
||||||
private final Lazy<PreferencesComponent> preferencesWindow;
|
private final Lazy<PreferencesComponent> preferencesWindow;
|
||||||
private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
|
private final Lazy<QuitComponent> quitWindow;
|
||||||
private final Provider<QuitComponent.Builder> quitWindowBuilderProvider;
|
private final Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider;
|
||||||
|
private final Provider<LockComponent.Builder> lockWorkflowBuilderProvider;
|
||||||
|
private final ErrorComponent.Builder errorWindowBuilder;
|
||||||
private final Optional<TrayIntegrationProvider> trayIntegration;
|
private final Optional<TrayIntegrationProvider> trayIntegration;
|
||||||
private final Optional<UiAppearanceProvider> appearanceProvider;
|
private final Optional<UiAppearanceProvider> appearanceProvider;
|
||||||
private final VaultService vaultService;
|
private final VaultService vaultService;
|
||||||
private final LicenseHolder licenseHolder;
|
private final LicenseHolder licenseHolder;
|
||||||
private final BooleanBinding hasVisibleStages;
|
private final ObservableList<Window> visibleWindows;
|
||||||
|
private final BooleanBinding hasVisibleWindows;
|
||||||
private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
|
private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<QuitComponent.Builder> quitWindowBuilderProvider, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder, ObservableSet<Stage> visibleStages) {
|
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider, Provider<LockComponent.Builder> lockWorkflowBuilderProvider, Lazy<QuitComponent> quitWindow, ErrorComponent.Builder errorWindowBuilder, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) {
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.mainWindow = mainWindow;
|
this.mainWindow = mainWindow;
|
||||||
this.preferencesWindow = preferencesWindow;
|
this.preferencesWindow = preferencesWindow;
|
||||||
this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
|
this.unlockWorkflowBuilderProvider = unlockWorkflowBuilderProvider;
|
||||||
this.quitWindowBuilderProvider = quitWindowBuilderProvider;
|
this.lockWorkflowBuilderProvider = lockWorkflowBuilderProvider;
|
||||||
|
this.quitWindow = quitWindow;
|
||||||
|
this.errorWindowBuilder = errorWindowBuilder;
|
||||||
this.trayIntegration = trayIntegration;
|
this.trayIntegration = trayIntegration;
|
||||||
this.appearanceProvider = appearanceProvider;
|
this.appearanceProvider = appearanceProvider;
|
||||||
this.vaultService = vaultService;
|
this.vaultService = vaultService;
|
||||||
this.licenseHolder = licenseHolder;
|
this.licenseHolder = licenseHolder;
|
||||||
this.hasVisibleStages = Bindings.isNotEmpty(visibleStages);
|
this.visibleWindows = Stage.getWindows().filtered(Window::isShowing);
|
||||||
|
this.hasVisibleWindows = Bindings.isNotEmpty(visibleWindows);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
LOG.trace("FxApplication.start()");
|
LOG.trace("FxApplication.start()");
|
||||||
Platform.setImplicitExit(false);
|
Platform.setImplicitExit(false);
|
||||||
|
|
||||||
EasyBind.subscribe(hasVisibleStages, this::hasVisibleStagesChanged);
|
hasVisibleWindows.addListener(this::hasVisibleStagesChanged);
|
||||||
|
|
||||||
settings.theme().addListener(this::appThemeChanged);
|
settings.theme().addListener(this::appThemeChanged);
|
||||||
loadSelectedStyleSheet(settings.theme().get());
|
loadSelectedStyleSheet(settings.theme().get());
|
||||||
@@ -78,7 +89,8 @@ public class FxApplication extends Application {
|
|||||||
throw new UnsupportedOperationException("Use start() instead.");
|
throw new UnsupportedOperationException("Use start() instead.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hasVisibleStagesChanged(boolean newValue) {
|
private void hasVisibleStagesChanged(@SuppressWarnings("unused") ObservableValue<? extends Boolean> observableValue, @SuppressWarnings("unused") boolean oldValue, boolean newValue) {
|
||||||
|
LOG.debug("has visible stages: {}", newValue);
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
trayIntegration.ifPresent(TrayIntegrationProvider::restoredFromTray);
|
trayIntegration.ifPresent(TrayIntegrationProvider::restoredFromTray);
|
||||||
} else {
|
} else {
|
||||||
@@ -93,23 +105,41 @@ public class FxApplication extends Application {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showMainWindow() {
|
public CompletionStage<Stage> showMainWindow() {
|
||||||
|
CompletableFuture<Stage> future = new CompletableFuture<>();
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
mainWindow.get().showMainWindow();
|
var win = mainWindow.get().showMainWindow();
|
||||||
LOG.debug("Showing MainWindow");
|
LOG.debug("Showing MainWindow");
|
||||||
|
future.complete(win);
|
||||||
});
|
});
|
||||||
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) {
|
public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
unlockWindowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
|
if (vault.stateProperty().transition(VaultState.Value.LOCKED, VaultState.Value.PROCESSING)) {
|
||||||
LOG.debug("Showing UnlockWindow for {}", vault.getDisplayName());
|
unlockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
|
||||||
|
LOG.debug("Start unlock workflow for {}", vault.getDisplayName());
|
||||||
|
} else {
|
||||||
|
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to unlock vault in non-locked state.")));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startLockWorkflow(Vault vault, Optional<Stage> owner) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING)) {
|
||||||
|
lockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
|
||||||
|
LOG.debug("Start lock workflow for {}", vault.getDisplayName());
|
||||||
|
} else {
|
||||||
|
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to lock vault in non-unlocked state.")));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showQuitWindow(QuitResponse response) {
|
public void showQuitWindow(QuitResponse response) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
quitWindowBuilderProvider.get().quitResponse(response).build().showQuitWindow();
|
quitWindow.get().showQuitWindow(response);
|
||||||
LOG.debug("Showing QuitWindow");
|
LOG.debug("Showing QuitWindow");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -119,13 +149,13 @@ public class FxApplication extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void appThemeChanged(@SuppressWarnings("unused") ObservableValue<? extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) {
|
private void appThemeChanged(@SuppressWarnings("unused") ObservableValue<? extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) {
|
||||||
appearanceProvider.ifPresent(appearanceProvider -> {
|
if (appearanceProvider.isPresent() && oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC) {
|
||||||
try {
|
try {
|
||||||
appearanceProvider.removeListener(systemInterfaceThemeListener);
|
appearanceProvider.get().removeListener(systemInterfaceThemeListener);
|
||||||
} catch (UiAppearanceException e) {
|
} catch (UiAppearanceException e) {
|
||||||
LOG.error("Failed to disable automatic theme switching.");
|
LOG.error("Failed to disable automatic theme switching.");
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
loadSelectedStyleSheet(newValue);
|
loadSelectedStyleSheet(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,8 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.ui.fxapp;
|
package org.cryptomator.ui.fxapp;
|
||||||
|
|
||||||
import dagger.BindsInstance;
|
|
||||||
import dagger.Subcomponent;
|
import dagger.Subcomponent;
|
||||||
|
|
||||||
import javax.inject.Named;
|
|
||||||
|
|
||||||
@FxApplicationScoped
|
@FxApplicationScoped
|
||||||
@Subcomponent(modules = FxApplicationModule.class)
|
@Subcomponent(modules = FxApplicationModule.class)
|
||||||
public interface FxApplicationComponent {
|
public interface FxApplicationComponent {
|
||||||
@@ -19,9 +16,6 @@ public interface FxApplicationComponent {
|
|||||||
@Subcomponent.Builder
|
@Subcomponent.Builder
|
||||||
interface Builder {
|
interface Builder {
|
||||||
|
|
||||||
@BindsInstance
|
|
||||||
Builder trayMenuSupported(@Named("trayMenuSupported") boolean trayMenuSupported);
|
|
||||||
|
|
||||||
FxApplicationComponent build();
|
FxApplicationComponent build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import dagger.Provides;
|
|||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
import org.cryptomator.ui.common.ErrorComponent;
|
import org.cryptomator.ui.common.ErrorComponent;
|
||||||
import org.cryptomator.ui.common.StageFactory;
|
import org.cryptomator.ui.common.StageFactory;
|
||||||
|
import org.cryptomator.ui.lock.LockComponent;
|
||||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||||
import org.cryptomator.ui.preferences.PreferencesComponent;
|
import org.cryptomator.ui.preferences.PreferencesComponent;
|
||||||
import org.cryptomator.ui.quit.QuitComponent;
|
import org.cryptomator.ui.quit.QuitComponent;
|
||||||
@@ -18,7 +19,6 @@ import org.cryptomator.ui.unlock.UnlockComponent;
|
|||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableSet;
|
import javafx.collections.ObservableSet;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
@@ -28,15 +28,9 @@ import java.io.UncheckedIOException;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, QuitComponent.class, ErrorComponent.class})
|
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class})
|
||||||
abstract class FxApplicationModule {
|
abstract class FxApplicationModule {
|
||||||
|
|
||||||
@Provides
|
|
||||||
@FxApplicationScoped
|
|
||||||
static ObservableSet<Stage> provideVisibleStages() {
|
|
||||||
return FXCollections.observableSet();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Named("windowIcons")
|
@Named("windowIcons")
|
||||||
@FxApplicationScoped
|
@FxApplicationScoped
|
||||||
@@ -56,16 +50,9 @@ abstract class FxApplicationModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@FxApplicationScoped
|
@FxApplicationScoped
|
||||||
static StageFactory provideStageFactory(@Named("windowIcons") List<Image> windowIcons, ObservableSet<Stage> visibleStages) {
|
static StageFactory provideStageFactory(@Named("windowIcons") List<Image> windowIcons) {
|
||||||
return new StageFactory(stage -> {
|
return new StageFactory(stage -> {
|
||||||
stage.getIcons().addAll(windowIcons);
|
stage.getIcons().addAll(windowIcons);
|
||||||
stage.showingProperty().addListener((observableValue, wasShowing, isShowing) -> {
|
|
||||||
if (isShowing) {
|
|
||||||
visibleStages.add(stage);
|
|
||||||
} else {
|
|
||||||
visibleStages.remove(stage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,4 +75,8 @@ abstract class FxApplicationModule {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static QuitComponent provideQuitComponent(QuitComponent.Builder builder) {
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import javafx.beans.property.StringProperty;
|
|||||||
import javafx.concurrent.ScheduledService;
|
import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
@@ -39,8 +40,13 @@ public abstract class UpdateCheckerModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@FxApplicationScoped
|
@FxApplicationScoped
|
||||||
static HttpClient provideHttpClient() {
|
static Optional<HttpClient> provideHttpClient() {
|
||||||
return HttpClient.newHttpClient();
|
try {
|
||||||
|
return Optional.of(HttpClient.newHttpClient());
|
||||||
|
} catch (UncheckedIOException e) {
|
||||||
|
LOG.error("HttpClient for update check cannot be created.", e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -66,11 +72,20 @@ public abstract class UpdateCheckerModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@FxApplicationScoped
|
@FxApplicationScoped
|
||||||
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, HttpClient httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
|
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, Optional<HttpClient> httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
|
||||||
ScheduledService<String> service = new ScheduledService<>() {
|
ScheduledService<String> service = new ScheduledService<>() {
|
||||||
@Override
|
@Override
|
||||||
protected Task<String> createTask() {
|
protected Task<String> createTask() {
|
||||||
return new UpdateCheckerTask(httpClient, checkForUpdatesRequest);
|
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.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException()));
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.base.Suppliers;
|
||||||
|
import dagger.Lazy;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.concurrent.Service;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class BatchService extends Service<Void> {
|
||||||
|
|
||||||
|
private final Iterator<HealthCheckTask> remainingTasks;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public BatchService(Iterable<HealthCheckTask> tasks) {
|
||||||
|
this.remainingTasks = tasks.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<Void> createTask() {
|
||||||
|
Preconditions.checkState(remainingTasks.hasNext(), "No remaining tasks");
|
||||||
|
return remainingTasks.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void succeeded() {
|
||||||
|
if (remainingTasks.hasNext()) {
|
||||||
|
this.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import com.tobiasdiez.easybind.EasyBind;
|
||||||
|
import com.tobiasdiez.easybind.EasyObservableList;
|
||||||
|
import com.tobiasdiez.easybind.Subscription;
|
||||||
|
import com.tobiasdiez.easybind.optional.OptionalBinding;
|
||||||
|
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.beans.binding.Binding;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.concurrent.Worker;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
public class CheckDetailController implements FxController {
|
||||||
|
|
||||||
|
private final EasyObservableList<DiagnosticResult> results;
|
||||||
|
private final OptionalBinding<Worker.State> taskState;
|
||||||
|
private final Binding<String> taskName;
|
||||||
|
private final Binding<Number> taskDuration;
|
||||||
|
private final ResultListCellFactory resultListCellFactory;
|
||||||
|
private final Binding<Boolean> taskRunning;
|
||||||
|
private final Binding<Boolean> taskScheduled;
|
||||||
|
private final Binding<Boolean> taskFinished;
|
||||||
|
private final Binding<Boolean> taskNotStarted;
|
||||||
|
private final Binding<Boolean> taskSucceeded;
|
||||||
|
private final Binding<Boolean> taskFailed;
|
||||||
|
private final Binding<Boolean> taskCancelled;
|
||||||
|
private final Binding<Number> countOfWarnSeverity;
|
||||||
|
private final Binding<Number> countOfCritSeverity;
|
||||||
|
|
||||||
|
public ListView<DiagnosticResult> resultsListView;
|
||||||
|
private Subscription resultSubscription;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public CheckDetailController(ObjectProperty<HealthCheckTask> selectedTask, ResultListCellFactory resultListCellFactory) {
|
||||||
|
this.results = EasyBind.wrapList(FXCollections.observableArrayList());
|
||||||
|
this.taskState = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::stateProperty);
|
||||||
|
this.taskName = EasyBind.wrapNullable(selectedTask).map(HealthCheckTask::getTitle).orElse("");
|
||||||
|
this.taskDuration = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::durationInMillisProperty).orElse(-1L);
|
||||||
|
this.resultListCellFactory = resultListCellFactory;
|
||||||
|
this.taskRunning = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::runningProperty).orElse(false); //TODO: DOES NOT WORK
|
||||||
|
this.taskScheduled = taskState.map(Worker.State.SCHEDULED::equals).orElse(false);
|
||||||
|
this.taskNotStarted = taskState.map(Worker.State.READY::equals).orElse(false);
|
||||||
|
this.taskSucceeded = taskState.map(Worker.State.SUCCEEDED::equals).orElse(false);
|
||||||
|
this.taskFailed = taskState.map(Worker.State.FAILED::equals).orElse(false);
|
||||||
|
this.taskCancelled = taskState.map(Worker.State.CANCELLED::equals).orElse(false);
|
||||||
|
this.taskFinished = EasyBind.combine(taskSucceeded, taskFailed, taskCancelled, (a, b, c) -> a || b || c);
|
||||||
|
this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN));
|
||||||
|
this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL));
|
||||||
|
selectedTask.addListener(this::selectedTaskChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectedTaskChanged(ObservableValue<? extends HealthCheckTask> observable, HealthCheckTask oldValue, HealthCheckTask newValue) {
|
||||||
|
if (resultSubscription != null) {
|
||||||
|
resultSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
if (newValue != null) {
|
||||||
|
resultSubscription = EasyBind.bindContent(results, newValue.results());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Function<Stream<? extends DiagnosticResult>, Long> countSeverity(DiagnosticResult.Severity severity) {
|
||||||
|
return stream -> stream.filter(item -> severity.equals(item.getServerity())).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
resultsListView.setItems(results);
|
||||||
|
resultsListView.setCellFactory(resultListCellFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Getter/Setter */
|
||||||
|
|
||||||
|
public String getTaskName() {
|
||||||
|
return taskName.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<String> taskNameProperty() {
|
||||||
|
return taskName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Number getTaskDuration() {
|
||||||
|
return taskDuration.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Number> taskDurationProperty() {
|
||||||
|
return taskDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCountOfWarnSeverity() {
|
||||||
|
return countOfWarnSeverity.getValue().longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Number> countOfWarnSeverityProperty() {
|
||||||
|
return countOfWarnSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCountOfCritSeverity() {
|
||||||
|
return countOfCritSeverity.getValue().longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Number> countOfCritSeverityProperty() {
|
||||||
|
return countOfCritSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskRunning() {
|
||||||
|
return taskRunning.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskRunningProperty() {
|
||||||
|
return taskRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskFinished() {
|
||||||
|
return taskFinished.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskFinishedProperty() {
|
||||||
|
return taskFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskScheduled() {
|
||||||
|
return taskScheduled.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskScheduledProperty() {
|
||||||
|
return taskScheduled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskNotStarted() {
|
||||||
|
return taskNotStarted.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskNotStartedProperty() {
|
||||||
|
return taskNotStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskSucceeded() {
|
||||||
|
return taskSucceeded.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskSucceededProperty() {
|
||||||
|
return taskSucceeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskFailed() {
|
||||||
|
return taskFailed.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskFailedProperty() {
|
||||||
|
return taskFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskCancelled() {
|
||||||
|
return taskCancelled.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskCancelledProperty() {
|
||||||
|
return taskCancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||||
|
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.concurrent.Worker;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.ContentDisplay;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
|
|
||||||
|
class CheckListCell extends ListCell<HealthCheckTask> {
|
||||||
|
|
||||||
|
private final FontAwesome5IconView stateIcon = new FontAwesome5IconView();
|
||||||
|
|
||||||
|
CheckListCell() {
|
||||||
|
setPadding(new Insets(6));
|
||||||
|
setAlignment(Pos.CENTER_LEFT);
|
||||||
|
setContentDisplay(ContentDisplay.LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(HealthCheckTask item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
|
if (item != null) {
|
||||||
|
textProperty().bind(item.titleProperty());
|
||||||
|
item.stateProperty().addListener(this::stateChanged);
|
||||||
|
graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()),item.stateProperty()));
|
||||||
|
stateIcon.setGlyph(glyphForState(item.getState()));
|
||||||
|
} else {
|
||||||
|
textProperty().unbind();
|
||||||
|
graphicProperty().unbind();
|
||||||
|
setGraphic(null);
|
||||||
|
setText(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stateChanged(ObservableValue<? extends Worker.State> observable, Worker.State oldState, Worker.State newState) {
|
||||||
|
stateIcon.setGlyph(glyphForState(newState));
|
||||||
|
stateIcon.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node graphicForState(Worker.State state) {
|
||||||
|
return switch (state) {
|
||||||
|
case READY -> null;
|
||||||
|
case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private FontAwesome5Icon glyphForState(Worker.State state) {
|
||||||
|
return switch (state) {
|
||||||
|
case READY -> FontAwesome5Icon.COG; //just a placeholder
|
||||||
|
case SCHEDULED -> FontAwesome5Icon.CLOCK;
|
||||||
|
case RUNNING -> FontAwesome5Icon.SPINNER;
|
||||||
|
case FAILED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
|
||||||
|
case CANCELLED -> FontAwesome5Icon.BAN;
|
||||||
|
case SUCCEEDED -> FontAwesome5Icon.CHECK;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.tobiasdiez.easybind.EasyBind;
|
||||||
|
import dagger.Lazy;
|
||||||
|
import org.cryptomator.ui.common.ErrorComponent;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.beans.binding.Binding;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.concurrent.Worker;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.scene.control.cell.CheckBoxListCell;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.util.StringConverter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
public class CheckListController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(CheckListController.class);
|
||||||
|
private static final Set<Worker.State> END_STATES = Set.of(Worker.State.FAILED, Worker.State.CANCELLED, Worker.State.SUCCEEDED);
|
||||||
|
|
||||||
|
private final Stage window;
|
||||||
|
private final ObservableList<HealthCheckTask> tasks;
|
||||||
|
private final ReportWriter reportWriter;
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
private final ObjectProperty<HealthCheckTask> selectedTask;
|
||||||
|
private final Lazy<ErrorComponent.Builder> errorComponenBuilder;
|
||||||
|
private final SimpleObjectProperty<Worker<?>> runningTask;
|
||||||
|
private final Binding<Boolean> running;
|
||||||
|
private final Binding<Boolean> finished;
|
||||||
|
private final Map<HealthCheckTask, BooleanProperty> listPickIndicators;
|
||||||
|
private final IntegerProperty numberOfPickedChecks;
|
||||||
|
private final BooleanBinding anyCheckSelected;
|
||||||
|
private final BooleanProperty showResultScreen;
|
||||||
|
|
||||||
|
/* FXML */
|
||||||
|
public ListView<HealthCheckTask> checksListView;
|
||||||
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public CheckListController(@HealthCheckWindow Stage window, Lazy<Collection<HealthCheckTask>> tasks, ReportWriter reportWriteTask, ObjectProperty<HealthCheckTask> selectedTask, ExecutorService executorService, Lazy<ErrorComponent.Builder> errorComponenBuilder) {
|
||||||
|
this.window = window;
|
||||||
|
this.tasks = FXCollections.observableArrayList(tasks.get());
|
||||||
|
this.reportWriter = reportWriteTask;
|
||||||
|
this.executorService = executorService;
|
||||||
|
this.selectedTask = selectedTask;
|
||||||
|
this.errorComponenBuilder = errorComponenBuilder;
|
||||||
|
this.runningTask = new SimpleObjectProperty<>();
|
||||||
|
this.running = EasyBind.wrapNullable(runningTask).mapObservable(Worker::runningProperty).orElse(false);
|
||||||
|
this.finished = EasyBind.wrapNullable(runningTask).mapObservable(Worker::stateProperty).map(END_STATES::contains).orElse(false);
|
||||||
|
this.listPickIndicators = new HashMap<>();
|
||||||
|
this.numberOfPickedChecks = new SimpleIntegerProperty(0);
|
||||||
|
this.tasks.forEach(task -> {
|
||||||
|
var entrySelectedProp = new SimpleBooleanProperty(false);
|
||||||
|
entrySelectedProp.addListener((observable, oldValue, newValue) -> numberOfPickedChecks.set(numberOfPickedChecks.get() + (newValue ? 1 : -1)));
|
||||||
|
listPickIndicators.put(task, entrySelectedProp);
|
||||||
|
});
|
||||||
|
this.anyCheckSelected = selectedTask.isNotNull();
|
||||||
|
this.showResultScreen = new SimpleBooleanProperty(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
checksListView.setItems(tasks);
|
||||||
|
checksListView.setCellFactory(CheckBoxListCell.forListView(listPickIndicators::get, new StringConverter<HealthCheckTask>() {
|
||||||
|
@Override
|
||||||
|
public String toString(HealthCheckTask object) {
|
||||||
|
return object.getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckTask fromString(String string) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void toggleSelectAll(ActionEvent event) {
|
||||||
|
if (event.getSource() instanceof CheckBox c) {
|
||||||
|
listPickIndicators.forEach( (task, pickProperty) -> pickProperty.set(c.isSelected()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void runSelectedChecks() {
|
||||||
|
Preconditions.checkState(runningTask.get() == null);
|
||||||
|
var batch = checksListView.getItems().filtered(item -> listPickIndicators.get(item).get());
|
||||||
|
var batchService = new BatchService(batch);
|
||||||
|
batchService.setExecutor(executorService);
|
||||||
|
batchService.start();
|
||||||
|
runningTask.set(batchService);
|
||||||
|
showResultScreen.set(true);
|
||||||
|
checksListView.getSelectionModel().select(batch.get(0));
|
||||||
|
checksListView.setCellFactory(view -> new CheckListCell());
|
||||||
|
window.sizeToScene();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public synchronized void cancelCheck() {
|
||||||
|
Preconditions.checkState(runningTask.get() != null);
|
||||||
|
runningTask.get().cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void exportResults() {
|
||||||
|
try {
|
||||||
|
reportWriter.writeReport(tasks);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Failed to write health check report.", e);
|
||||||
|
errorComponenBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Getter/Setter */
|
||||||
|
public boolean isRunning() {
|
||||||
|
return running.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> runningProperty() {
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFinished() {
|
||||||
|
return finished.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> finishedProperty() {
|
||||||
|
return finished;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAnyCheckSelected() {
|
||||||
|
return anyCheckSelected.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding anyCheckSelectedProperty() {
|
||||||
|
return anyCheckSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getShowResultScreen() {
|
||||||
|
return showResultScreen.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty showResultScreenProperty() {
|
||||||
|
return showResultScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNumberOfPickedChecks() {
|
||||||
|
return numberOfPickedChecks.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerProperty numberOfPickedChecksProperty() {
|
||||||
|
return numberOfPickedChecks;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import dagger.BindsInstance;
|
||||||
|
import dagger.Lazy;
|
||||||
|
import dagger.Subcomponent;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
@Subcomponent(modules = {HealthCheckModule.class})
|
||||||
|
public interface HealthCheckComponent {
|
||||||
|
|
||||||
|
@HealthCheckWindow
|
||||||
|
Stage window();
|
||||||
|
|
||||||
|
@FxmlScene(FxmlFile.HEALTH_START)
|
||||||
|
Lazy<Scene> scene();
|
||||||
|
|
||||||
|
default Stage showHealthCheckWindow() {
|
||||||
|
Stage stage = window();
|
||||||
|
stage.setScene(scene().get());
|
||||||
|
stage.show();
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subcomponent.Builder
|
||||||
|
interface Builder {
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder vault(@HealthCheckWindow Vault vault);
|
||||||
|
|
||||||
|
HealthCheckComponent build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import dagger.Binds;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoMap;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
import org.cryptomator.cryptofs.health.api.HealthCheck;
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
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.StageFactory;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||||
|
import org.cryptomator.ui.mainwindow.MainWindow;
|
||||||
|
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@Module(subcomponents = {KeyLoadingComponent.class})
|
||||||
|
abstract class HealthCheckModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static AtomicReference<Masterkey> provideMasterkeyRef() {
|
||||||
|
return new AtomicReference<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static AtomicReference<VaultConfig> provideVaultConfigRef() {
|
||||||
|
return new AtomicReference<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static Collection<HealthCheck> provideAvailableHealthChecks() {
|
||||||
|
return HealthCheck.allChecks();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static ObjectProperty<HealthCheckTask> provideSelectedHealthCheckTask() {
|
||||||
|
return new SimpleObjectProperty<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only inject with Lazy-Wrapper!*/
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static Collection<HealthCheckTask> provideAvailableHealthCheckTasks(Collection<HealthCheck> availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) {
|
||||||
|
return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckWindow
|
||||||
|
@HealthCheckScoped
|
||||||
|
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @HealthCheckWindow Stage window) {
|
||||||
|
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckWindow
|
||||||
|
@HealthCheckScoped
|
||||||
|
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||||
|
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckWindow
|
||||||
|
@HealthCheckScoped
|
||||||
|
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener<Boolean> showingListener) {
|
||||||
|
Stage stage = factory.create();
|
||||||
|
stage.setTitle(resourceBundle.getString("health.title"));
|
||||||
|
stage.setResizable(true);
|
||||||
|
stage.initModality(Modality.WINDOW_MODAL);
|
||||||
|
stage.initOwner(owner);
|
||||||
|
stage.showingProperty().addListener(showingListener); // bind masterkey lifecycle to window
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static ChangeListener<Boolean> provideWindowShowingChangeListener(AtomicReference<Masterkey> masterkey) {
|
||||||
|
return (observable, wasShowing, isShowing) -> {
|
||||||
|
if (!isShowing) {
|
||||||
|
Optional.ofNullable(masterkey.getAndSet(null)).ifPresent(Masterkey::destroy);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.HEALTH_START)
|
||||||
|
@HealthCheckScoped
|
||||||
|
static Scene provideHealthStartScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
|
return fxmlLoaders.createScene(FxmlFile.HEALTH_START);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.HEALTH_CHECK_LIST)
|
||||||
|
@HealthCheckScoped
|
||||||
|
static Scene provideHealthCheckListScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
|
return fxmlLoaders.createScene(FxmlFile.HEALTH_CHECK_LIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(StartController.class)
|
||||||
|
abstract FxController bindStartController(StartController controller);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(CheckListController.class)
|
||||||
|
abstract FxController bindCheckController(CheckListController controller);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(CheckDetailController.class)
|
||||||
|
abstract FxController bindCheckDetailController(CheckDetailController controller);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(ResultListCellController.class)
|
||||||
|
abstract FxController bindResultListCellController(ResultListCellController controller);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import javax.inject.Scope;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
@Scope
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@interface HealthCheckScoped {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||||
|
import org.cryptomator.cryptofs.health.api.HealthCheck;
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.MissingResourceException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
|
||||||
|
class HealthCheckTask extends Task<Void> {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(HealthCheckTask.class);
|
||||||
|
|
||||||
|
private final Path vaultPath;
|
||||||
|
private final VaultConfig vaultConfig;
|
||||||
|
private final Masterkey masterkey;
|
||||||
|
private final SecureRandom csprng;
|
||||||
|
private final HealthCheck check;
|
||||||
|
private final ObservableList<DiagnosticResult> results;
|
||||||
|
private final LongProperty durationInMillis;
|
||||||
|
|
||||||
|
public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, ResourceBundle resourceBundle) {
|
||||||
|
this.vaultPath = Objects.requireNonNull(vaultPath);
|
||||||
|
this.vaultConfig = Objects.requireNonNull(vaultConfig);
|
||||||
|
this.masterkey = Objects.requireNonNull(masterkey);
|
||||||
|
this.csprng = Objects.requireNonNull(csprng);
|
||||||
|
this.check = Objects.requireNonNull(check);
|
||||||
|
this.results = FXCollections.observableArrayList();
|
||||||
|
try {
|
||||||
|
updateTitle(resourceBundle.getString("health." + check.identifier()));
|
||||||
|
} catch (MissingResourceException e) {
|
||||||
|
LOG.warn("Missing proper name for health check {}, falling back to default.", check.identifier());
|
||||||
|
updateTitle(check.identifier());
|
||||||
|
}
|
||||||
|
this.durationInMillis = new SimpleLongProperty(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void call() {
|
||||||
|
Instant start = Instant.now();
|
||||||
|
try (var masterkeyClone = masterkey.clone(); //
|
||||||
|
var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
|
||||||
|
check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, result -> {
|
||||||
|
if (isCancelled()) {
|
||||||
|
throw new CancellationException();
|
||||||
|
}
|
||||||
|
// FIXME: slowdown for demonstration purposes only:
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Platform.runLater(() -> results.add(result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Platform.runLater(() ->durationInMillis.set(Duration.between(start, Instant.now()).toMillis()));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void scheduled() {
|
||||||
|
LOG.info("starting {}", check.identifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void done() {
|
||||||
|
LOG.info("finished {}", check.identifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Getter */
|
||||||
|
|
||||||
|
public ObservableList<DiagnosticResult> results() {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HealthCheck getCheck() {
|
||||||
|
return check;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LongProperty durationInMillisProperty() {
|
||||||
|
return durationInMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDurationInMillis() {
|
||||||
|
return durationInMillis.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@interface HealthCheckWindow {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
|
import org.cryptomator.common.Environment;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.concurrent.Worker;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
public class ReportWriter {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ReportWriter.class);
|
||||||
|
private static final String REPORT_HEADER = """
|
||||||
|
**************************************
|
||||||
|
* Cryptomator Vault Health Report *
|
||||||
|
**************************************
|
||||||
|
Analyzed vault: %s (Current name "%s")
|
||||||
|
Vault storage path: %s
|
||||||
|
""";
|
||||||
|
private static final String REPORT_CHECK_HEADER = """
|
||||||
|
|
||||||
|
|
||||||
|
Check %s
|
||||||
|
------------------------------
|
||||||
|
""";
|
||||||
|
private static final String REPORT_CHECK_RESULT = "%8s - %s\n";
|
||||||
|
private static final DateTimeFormatter TIME_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").withZone(ZoneId.systemDefault());
|
||||||
|
|
||||||
|
private final Vault vault;
|
||||||
|
private final VaultConfig vaultConfig;
|
||||||
|
private final Application application;
|
||||||
|
private final Path exportDestination;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ReportWriter(@HealthCheckWindow Vault vault, AtomicReference<VaultConfig> vaultConfigRef, Application application, Environment env) {
|
||||||
|
this.vault = vault;
|
||||||
|
this.vaultConfig = Objects.requireNonNull(vaultConfigRef.get());
|
||||||
|
this.application = application;
|
||||||
|
this.exportDestination = env.getLogDir().orElse(Path.of(System.getProperty("user.home"))).resolve("healthReport_" + vault.getDisplayName() + "_" + TIME_STAMP.format(Instant.now()) + ".log");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void writeReport(Collection<HealthCheckTask> tasks) throws IOException {
|
||||||
|
try (var out = Files.newOutputStream(exportDestination, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); //
|
||||||
|
var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
|
||||||
|
writer.write(REPORT_HEADER.formatted(vaultConfig.getId(), vault.getDisplayName(), vault.getPath()));
|
||||||
|
for (var task : tasks) {
|
||||||
|
if (task.getState() == Worker.State.READY) {
|
||||||
|
LOG.debug("Skipping not performed check {}.", task.getCheck().identifier());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
writer.write(REPORT_CHECK_HEADER.formatted(task.getCheck().identifier()));
|
||||||
|
switch (task.getState()) {
|
||||||
|
case SUCCEEDED -> {
|
||||||
|
writer.write("STATUS: SUCCESS\nRESULTS:\n");
|
||||||
|
for (var result : task.results()) {
|
||||||
|
writer.write(REPORT_CHECK_RESULT.formatted(result.getServerity(), result.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case CANCELLED -> writer.write("STATUS: CANCELED\n");
|
||||||
|
case FAILED -> {
|
||||||
|
writer.write("STATUS: FAILED\nREASON:\n" + task.getCheck().identifier());
|
||||||
|
writer.write(prepareFailureMsg(task));
|
||||||
|
}
|
||||||
|
case RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reveal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String prepareFailureMsg(HealthCheckTask task) {
|
||||||
|
if (task.getException() != null) {
|
||||||
|
return ExceptionUtils.getStackTrace(task.getException()) //
|
||||||
|
.lines() //
|
||||||
|
.map(line -> "\t\t" + line + "\n") //
|
||||||
|
.collect(Collectors.joining());
|
||||||
|
} else {
|
||||||
|
return "Unknown reason of failure.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reveal() {
|
||||||
|
application.getHostServices().showDocument(exportDestination.getParent().toUri().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
class ResultFixApplier {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class);
|
||||||
|
|
||||||
|
private final Path vaultPath;
|
||||||
|
private final SecureRandom csprng;
|
||||||
|
private final Masterkey masterkey;
|
||||||
|
private final VaultConfig vaultConfig;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ResultFixApplier(@HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng) {
|
||||||
|
this.vaultPath = vault.getPath();
|
||||||
|
this.masterkey = masterkeyRef.get();
|
||||||
|
this.vaultConfig = vaultConfigRef.get();
|
||||||
|
this.csprng = csprng;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fix(DiagnosticResult result) {
|
||||||
|
Preconditions.checkArgument(result.getServerity() == DiagnosticResult.Severity.WARN, "Unfixable result");
|
||||||
|
try (var masterkeyClone = masterkey.clone(); //
|
||||||
|
var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
|
||||||
|
result.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Failed to apply fix", e);
|
||||||
|
Alert alert = new Alert(Alert.AlertType.ERROR, e.getMessage());
|
||||||
|
alert.showAndWait();
|
||||||
|
//TODO: real error/not supported handling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import com.tobiasdiez.easybind.EasyBind;
|
||||||
|
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||||
|
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.beans.binding.Binding;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
|
||||||
|
// unscoped because each cell needs its own controller
|
||||||
|
public class ResultListCellController implements FxController {
|
||||||
|
|
||||||
|
private final ResultFixApplier fixApplier;
|
||||||
|
private final ObjectProperty<DiagnosticResult> result;
|
||||||
|
private final Binding<String> description;
|
||||||
|
|
||||||
|
public FontAwesome5IconView iconView;
|
||||||
|
public Button actionButton;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ResultListCellController(ResultFixApplier fixApplier) {
|
||||||
|
this.result = new SimpleObjectProperty<>(null);
|
||||||
|
this.description = EasyBind.wrapNullable(result).map(DiagnosticResult::toString).orElse("");
|
||||||
|
this.fixApplier = fixApplier;
|
||||||
|
result.addListener(this::updateCellContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCellContent(ObservableValue<? extends DiagnosticResult> observable, DiagnosticResult oldVal, DiagnosticResult newVal) {
|
||||||
|
iconView.getStyleClass().clear();
|
||||||
|
actionButton.setVisible(false);
|
||||||
|
//TODO: see comment in case WARN
|
||||||
|
actionButton.setManaged(false);
|
||||||
|
switch (newVal.getServerity()) {
|
||||||
|
case INFO -> {
|
||||||
|
iconView.setGlyph(FontAwesome5Icon.INFO_CIRCLE);
|
||||||
|
iconView.getStyleClass().add("glyph-icon-muted");
|
||||||
|
}
|
||||||
|
case GOOD -> {
|
||||||
|
iconView.setGlyph(FontAwesome5Icon.CHECK);
|
||||||
|
iconView.getStyleClass().add("glyph-icon-primary");
|
||||||
|
}
|
||||||
|
case WARN -> {
|
||||||
|
iconView.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE);
|
||||||
|
iconView.getStyleClass().add("glyph-icon-orange");
|
||||||
|
//TODO: Neither is any fix implemented, nor it is ensured, that only fix is executed at a time with good ui indication
|
||||||
|
// before both are not fix, do not show the button
|
||||||
|
//actionButton.setVisible(true);
|
||||||
|
}
|
||||||
|
case CRITICAL -> {
|
||||||
|
iconView.setGlyph(FontAwesome5Icon.TIMES);
|
||||||
|
iconView.getStyleClass().add("glyph-icon-red");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void runResultAction() {
|
||||||
|
final var realResult = result.get();
|
||||||
|
if (realResult != null) {
|
||||||
|
fixApplier.fix(realResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Getter & Setter */
|
||||||
|
|
||||||
|
|
||||||
|
public DiagnosticResult getResult() {
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResult(DiagnosticResult result) {
|
||||||
|
this.result.set(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<DiagnosticResult> resultProperty() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<String> descriptionProperty() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
|
||||||
|
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||||
|
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.control.ContentDisplay;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.util.Callback;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
public class ResultListCellFactory implements Callback<ListView<DiagnosticResult>, ListCell<DiagnosticResult>> {
|
||||||
|
|
||||||
|
private final FxmlLoaderFactory fxmlLoaders;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ResultListCellFactory(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
|
this.fxmlLoaders = fxmlLoaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListCell<DiagnosticResult> call(ListView<DiagnosticResult> param) {
|
||||||
|
try {
|
||||||
|
FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_result_listcell.fxml");
|
||||||
|
return new ResultListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException("Failed to load /fxml/health_result_listcell.fxml.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Cell extends ListCell<DiagnosticResult> {
|
||||||
|
|
||||||
|
private final Parent node;
|
||||||
|
private final ResultListCellController controller;
|
||||||
|
|
||||||
|
public Cell(Parent node, ResultListCellController controller) {
|
||||||
|
this.node = node;
|
||||||
|
this.controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(DiagnosticResult item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (item == null || empty) {
|
||||||
|
setText(null);
|
||||||
|
setGraphic(null);
|
||||||
|
} else {
|
||||||
|
controller.setResult(item);
|
||||||
|
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||||
|
setGraphic(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfigLoadException;
|
||||||
|
import org.cryptomator.cryptofs.VaultKeyInvalidException;
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||||
|
import org.cryptomator.ui.common.ErrorComponent;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||||
|
import org.cryptomator.ui.unlock.UnlockCancelledException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
public class StartController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(StartController.class);
|
||||||
|
|
||||||
|
private final Stage window;
|
||||||
|
private final Optional<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||||
|
private final KeyLoadingStrategy keyLoadingStrategy;
|
||||||
|
private final ExecutorService executor;
|
||||||
|
private final AtomicReference<Masterkey> masterkeyRef;
|
||||||
|
private final AtomicReference<VaultConfig> vaultConfigRef;
|
||||||
|
private final Lazy<Scene> checkScene;
|
||||||
|
private final Lazy<ErrorComponent.Builder> errorComponent;
|
||||||
|
|
||||||
|
/* FXML */
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent) {
|
||||||
|
this.window = window;
|
||||||
|
this.keyLoadingStrategy = keyLoadingStrategy;
|
||||||
|
this.executor = executor;
|
||||||
|
this.masterkeyRef = masterkeyRef;
|
||||||
|
this.vaultConfigRef = vaultConfigRef;
|
||||||
|
this.checkScene = checkScene;
|
||||||
|
this.errorComponent = errorComponent;
|
||||||
|
|
||||||
|
//TODO: this is ugly
|
||||||
|
//idea: delay the loading of the vault config and show a spinner (something like "check/load config") and react to the result of the loading
|
||||||
|
//or: load vault config in a previous step to see if it is loadable.
|
||||||
|
VaultConfig.UnverifiedVaultConfig tmp;
|
||||||
|
try {
|
||||||
|
tmp = vault.getUnverifiedVaultConfig();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
tmp = null;
|
||||||
|
}
|
||||||
|
this.unverifiedVaultConfig = Optional.ofNullable(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void close() {
|
||||||
|
LOG.trace("StartController.close()");
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void next() {
|
||||||
|
LOG.trace("StartController.next()");
|
||||||
|
executor.submit(this::loadKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadKey() {
|
||||||
|
assert !Platform.isFxApplicationThread();
|
||||||
|
assert unverifiedVaultConfig.isPresent();
|
||||||
|
try (var masterkey = keyLoadingStrategy.loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) {
|
||||||
|
var unverifiedCfg = unverifiedVaultConfig.get();
|
||||||
|
var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion());
|
||||||
|
vaultConfigRef.set(verifiedCfg);
|
||||||
|
var old = masterkeyRef.getAndSet(masterkey.clone());
|
||||||
|
if (old != null) {
|
||||||
|
old.destroy();
|
||||||
|
}
|
||||||
|
Platform.runLater(this::loadedKey);
|
||||||
|
} catch (MasterkeyLoadingFailedException e) {
|
||||||
|
if (keyLoadingStrategy.recoverFromException(e)) {
|
||||||
|
// retry
|
||||||
|
loadKey();
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> loadingKeyFailed(e));
|
||||||
|
}
|
||||||
|
} catch (VaultKeyInvalidException e) {
|
||||||
|
Platform.runLater(() -> loadingKeyFailed(e));
|
||||||
|
} catch (VaultConfigLoadException e) {
|
||||||
|
Platform.runLater(() -> loadingKeyFailed(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadedKey() {
|
||||||
|
LOG.debug("Loaded valid key");
|
||||||
|
window.setScene(checkScene.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadingKeyFailed(Exception e) {
|
||||||
|
if (e instanceof UnlockCancelledException) {
|
||||||
|
// ok
|
||||||
|
} else if (e instanceof VaultKeyInvalidException) {
|
||||||
|
LOG.error("Invalid key"); //TODO: specific error screen
|
||||||
|
errorComponent.get().window(window).cause(e).build().showErrorScene();
|
||||||
|
} else {
|
||||||
|
LOG.error("Failed to load key.", e);
|
||||||
|
errorComponent.get().window(window).cause(e).build().showErrorScene();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInvalidConfig() {
|
||||||
|
return unverifiedVaultConfig.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.cryptomator.ui.keyloading;
|
||||||
|
|
||||||
|
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 KeyLoading {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package org.cryptomator.ui.keyloading;
|
||||||
|
|
||||||
|
import dagger.BindsInstance;
|
||||||
|
import dagger.Subcomponent;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||||
|
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
@KeyLoadingScoped
|
||||||
|
@Subcomponent(modules = {KeyLoadingModule.class})
|
||||||
|
public interface KeyLoadingComponent {
|
||||||
|
|
||||||
|
@KeyLoading
|
||||||
|
KeyLoadingStrategy keyloadingStrategy();
|
||||||
|
|
||||||
|
@Subcomponent.Builder
|
||||||
|
interface Builder {
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder vault(@KeyLoading Vault vault);
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder window(@KeyLoading Stage window);
|
||||||
|
|
||||||
|
KeyLoadingComponent build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.cryptomator.ui.keyloading;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
|
||||||
|
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||||
|
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
|
||||||
|
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
@Module(includes = {MasterkeyFileLoadingModule.class})
|
||||||
|
abstract class KeyLoadingModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@KeyLoading
|
||||||
|
@KeyLoadingScoped
|
||||||
|
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||||
|
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@KeyLoading
|
||||||
|
@KeyLoadingScoped
|
||||||
|
static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
|
||||||
|
try {
|
||||||
|
String scheme = vault.getUnverifiedVaultConfig().getKeyId().getScheme();
|
||||||
|
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
|
||||||
|
return strategies.getOrDefault(scheme, () -> fallback).get();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return KeyLoadingStrategy.failed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.cryptomator.ui.keyloading;
|
||||||
|
|
||||||
|
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 KeyLoadingScoped {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package org.cryptomator.ui.keyloading;
|
||||||
|
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable, stateful {@link MasterkeyLoader}, that can deal with certain exceptions.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface KeyLoadingStrategy extends MasterkeyLoader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a master key. This might be a long-running operation, as it may require user input or expensive computations.
|
||||||
|
* <p>
|
||||||
|
* If loading fails exceptionally, this strategy might be able to {@link #recoverFromException(MasterkeyLoadingFailedException) recover from this exception}, so it can be used in a further attempt.
|
||||||
|
*
|
||||||
|
* @param keyId An URI uniquely identifying the source and identity of the key
|
||||||
|
* @return The raw key bytes. Must not be null
|
||||||
|
* @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the loader to try and recover from an exception thrown during the last attempt.
|
||||||
|
*
|
||||||
|
* @param exception An exception thrown by {@link #loadKey(URI)}.
|
||||||
|
* @return <code>true</code> if this component was able to handle the exception and another attempt can be made to load a masterkey
|
||||||
|
*/
|
||||||
|
default boolean recoverFromException(MasterkeyLoadingFailedException exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release any ressources or do follow-up tasks after loading a key.
|
||||||
|
*
|
||||||
|
* @param unlockedSuccessfully <code>true</code> if successfully unlocked a vault with the loaded key
|
||||||
|
* @implNote This method might be invoked multiple times, depending on whether multiple attempts to load a key are started.
|
||||||
|
*/
|
||||||
|
default void cleanup(boolean unlockedSuccessfully) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A key loading strategy that will always fail by throwing a {@link MasterkeyLoadingFailedException}.
|
||||||
|
*
|
||||||
|
* @param exception The cause of the failure. If not alreay an {@link MasterkeyLoadingFailedException}, it will get wrapped.
|
||||||
|
* @return A new KeyLoadingStrategy that will always fail with an {@link MasterkeyLoadingFailedException}.
|
||||||
|
*/
|
||||||
|
static KeyLoadingStrategy failed(Exception exception) {
|
||||||
|
return keyid -> {
|
||||||
|
if (exception instanceof MasterkeyLoadingFailedException e) {
|
||||||
|
throw e;
|
||||||
|
} else {
|
||||||
|
throw new MasterkeyLoadingFailedException("Can not load key", exception);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||||
|
|
||||||
|
import org.cryptomator.common.keychain.KeychainManager;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import java.nio.CharBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@KeyLoadingScoped
|
||||||
|
class MasterkeyFileLoadingFinisher {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingFinisher.class);
|
||||||
|
|
||||||
|
private final Vault vault;
|
||||||
|
private final Optional<char[]> storedPassword;
|
||||||
|
private final AtomicReference<char[]> enteredPassword;
|
||||||
|
private final AtomicBoolean shouldSavePassword;
|
||||||
|
private final KeychainManager keychain;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional<char[]> storedPassword, AtomicReference<char[]> enteredPassword, @Named("savePassword") AtomicBoolean shouldSavePassword, KeychainManager keychain) {
|
||||||
|
this.vault = vault;
|
||||||
|
this.storedPassword = storedPassword;
|
||||||
|
this.enteredPassword = enteredPassword;
|
||||||
|
this.shouldSavePassword = shouldSavePassword;
|
||||||
|
this.keychain = keychain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cleanup(boolean successfullyUnlocked) {
|
||||||
|
if (successfullyUnlocked && shouldSavePassword.get()) {
|
||||||
|
savePasswordToSystemkeychain();
|
||||||
|
}
|
||||||
|
wipePassword(storedPassword.orElse(null));
|
||||||
|
wipePassword(enteredPassword.getAndSet(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void savePasswordToSystemkeychain() {
|
||||||
|
if (keychain.isSupported()) {
|
||||||
|
try {
|
||||||
|
keychain.storePassphrase(vault.getId(), CharBuffer.wrap(enteredPassword.get()));
|
||||||
|
} catch (KeychainAccessException e) {
|
||||||
|
LOG.error("Failed to store passphrase in system keychain.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void wipePassword(char[] pw) {
|
||||||
|
if (pw != null) {
|
||||||
|
Arrays.fill(pw, ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user