mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 16:51:28 +00:00
Compare commits
448 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65369bdbff | ||
|
|
d18e618ef1 | ||
|
|
6840a649c7 | ||
|
|
19ebc7e562 | ||
|
|
0fa052d2d1 | ||
|
|
402861b9c0 | ||
|
|
aca1666dea | ||
|
|
70f6a4877c | ||
|
|
85c5dc8dfb | ||
|
|
23c113948f | ||
|
|
d86ea20c31 | ||
|
|
c1f44f76b9 | ||
|
|
b2f27c0a3d | ||
|
|
b1d7cfc81b | ||
|
|
7cd5c66836 | ||
|
|
95cef34234 | ||
|
|
8977440697 | ||
|
|
d5eb84a000 | ||
|
|
81fb6b8794 | ||
|
|
92b390d5bb | ||
|
|
8867532210 | ||
|
|
77db435b4f | ||
|
|
aaa93239d0 | ||
|
|
93445e22d4 | ||
|
|
64897cfa21 | ||
|
|
7349a29831 | ||
|
|
73554b4759 | ||
|
|
0312f045aa | ||
|
|
1134c1b2ff | ||
|
|
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 | ||
|
|
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 | ||
|
|
b781cf6f25 | ||
|
|
9b653f488b | ||
|
|
17c580267f | ||
|
|
490d1b8f87 | ||
|
|
b0ad86f16b | ||
|
|
df2b4ac086 | ||
|
|
aee4ececba | ||
|
|
b2992aa6ae | ||
|
|
4c546d281a | ||
|
|
c90e445a67 | ||
|
|
d3c2b0509e | ||
|
|
c6d1c2ca6b | ||
|
|
86dec80726 | ||
|
|
9083976989 | ||
|
|
d9c5d76417 | ||
|
|
c19c3754c6 | ||
|
|
947e0e2369 | ||
|
|
6eca8f2e0c | ||
|
|
9f61ad1941 | ||
|
|
41492a951a | ||
|
|
cbbfec5531 | ||
|
|
d7a4dff080 | ||
|
|
8e56218fbb | ||
|
|
9c40386106 | ||
|
|
c865805f55 | ||
|
|
8e352301cd | ||
|
|
52db1e8bdd | ||
|
|
1947623be8 | ||
|
|
4f8602684d | ||
|
|
75f66e40bf | ||
|
|
73587437cb | ||
|
|
18f6e70bba | ||
|
|
e067b94aca | ||
|
|
b04d5669fb | ||
|
|
ad44af338d | ||
|
|
4f4ddbc3e0 | ||
|
|
4e9af1c713 | ||
|
|
9a94b6f95e | ||
|
|
371b826aa0 | ||
|
|
d3fb90bf6d | ||
|
|
d24c478faa | ||
|
|
48d0aa423c | ||
|
|
5695fa2916 | ||
|
|
bcea1a4b32 | ||
|
|
2f8d4568fc | ||
|
|
5b55318e52 | ||
|
|
459f2ea2c3 | ||
|
|
d26147a34a | ||
|
|
afb9d4f010 | ||
|
|
db3bf9723e | ||
|
|
3a5937b35f | ||
|
|
f5cddda7a8 | ||
|
|
e9fa1d6476 | ||
|
|
084c1881ba | ||
|
|
ce131e4653 | ||
|
|
a121c7d0fe | ||
|
|
603bf99b25 | ||
|
|
9076fe5b46 | ||
|
|
6848f1a38e | ||
|
|
f4103fc917 | ||
|
|
c838da9df4 | ||
|
|
78df4e24b3 | ||
|
|
c0e2d01297 | ||
|
|
313f8b36d2 | ||
|
|
343794c1fe | ||
|
|
813c01aaed | ||
|
|
510ea8a6f6 | ||
|
|
a22fbea467 | ||
|
|
f64144d1da | ||
|
|
c01dd225c9 | ||
|
|
18b408a83a | ||
|
|
efaf107b20 | ||
|
|
86cba5e8a7 | ||
|
|
f4cee6d161 | ||
|
|
28c40b2277 | ||
|
|
8853054ed4 | ||
|
|
3eb44b06af | ||
|
|
1554437884 | ||
|
|
268bae8879 | ||
|
|
b382e436e2 | ||
|
|
6ccd248675 | ||
|
|
c760e63995 | ||
|
|
959dee153e | ||
|
|
aa22f656e8 | ||
|
|
d24734e16f | ||
|
|
01522e8c97 | ||
|
|
14e056551d | ||
|
|
82a42ea183 | ||
|
|
bfde26be79 | ||
|
|
51e43a0567 | ||
|
|
9a375cb3f6 | ||
|
|
2b8f39a149 | ||
|
|
2720a999d1 | ||
|
|
f675bd5017 | ||
|
|
16be3e7a97 | ||
|
|
551d9d1af1 | ||
|
|
0acc8ddb0a | ||
|
|
f0e1bcb899 | ||
|
|
d331c30224 | ||
|
|
d0d39a1c72 | ||
|
|
8904df91b4 | ||
|
|
786365c095 | ||
|
|
eb8f5f523d | ||
|
|
9c22adc97d | ||
|
|
265212cc0f | ||
|
|
9c860cc17c | ||
|
|
7f63153bc1 | ||
|
|
9531d44ab5 | ||
|
|
253abd4930 | ||
|
|
022ba81f2f | ||
|
|
26160e9bbc | ||
|
|
0a32de2c92 | ||
|
|
60aeb6a04b | ||
|
|
0206fc0b8f | ||
|
|
06b3343a6d | ||
|
|
598bec3050 | ||
|
|
0e5d72c22a | ||
|
|
21f4fab9ed | ||
|
|
58e2ec4742 | ||
|
|
6b6867b826 | ||
|
|
92321a7569 | ||
|
|
7781572b6d | ||
|
|
b5efe39eb8 | ||
|
|
cae86cd71f | ||
|
|
667617b480 | ||
|
|
9329311491 | ||
|
|
22438d1eba | ||
|
|
d295084312 | ||
|
|
ab538cd215 | ||
|
|
f9cb438a0b | ||
|
|
354de225ed | ||
|
|
0b7f8d71ed | ||
|
|
9657a13912 | ||
|
|
c5a3b29554 | ||
|
|
ae957c2588 | ||
|
|
da84133732 | ||
|
|
10275eb80f | ||
|
|
29a9c7dffe | ||
|
|
d5b8996a39 | ||
|
|
f012977aac | ||
|
|
8680bd1dee | ||
|
|
528510eb53 | ||
|
|
e8d17cfb7b | ||
|
|
cdbcd20241 | ||
|
|
fef3ade5c6 | ||
|
|
194d7f0da9 | ||
|
|
8ad20c27da | ||
|
|
f891a9f1a7 | ||
|
|
2a335eb7af | ||
|
|
6578ac0121 | ||
|
|
83f9514a42 | ||
|
|
c92f1aa655 | ||
|
|
688993ba37 | ||
|
|
ddfc8d5bc3 | ||
|
|
c3141e3601 | ||
|
|
f49974180b | ||
|
|
eb6ade49f6 | ||
|
|
6553c04256 | ||
|
|
a8cb015a06 | ||
|
|
ee81dca71c | ||
|
|
ce06ddf312 | ||
|
|
fee2d4bc0c | ||
|
|
a2a990bd0c | ||
|
|
cedbe6523d | ||
|
|
ec64055d93 | ||
|
|
6f542ac8b8 | ||
|
|
8a3c101764 | ||
|
|
49a8689090 | ||
|
|
8f5c4997b1 | ||
|
|
900fdd7f6f | ||
|
|
4723416d45 | ||
|
|
8483f71f57 | ||
|
|
5dac594cc9 | ||
|
|
06f4e160d6 | ||
|
|
1d38ee2fcb | ||
|
|
921b70ebaa | ||
|
|
0f56d424da | ||
|
|
4cd43a2b1c | ||
|
|
1ef359e68b | ||
|
|
58ce1e45b8 | ||
|
|
bd854e7f7a | ||
|
|
3ecc0cf906 | ||
|
|
ea6925f905 | ||
|
|
f45b78d8c0 | ||
|
|
75ca8a91f8 | ||
|
|
16462d05c8 | ||
|
|
cbef54f05c | ||
|
|
f601ff4ce3 | ||
|
|
84fbf7392b | ||
|
|
bbf9379f56 | ||
|
|
6aa14fcfb1 | ||
|
|
710c7d0da7 | ||
|
|
841d4eb9c7 | ||
|
|
8c4d35d3db | ||
|
|
4110057fa1 | ||
|
|
9b1d6d2f2f | ||
|
|
1c45455cb2 | ||
|
|
411aca8695 | ||
|
|
3de69021df | ||
|
|
8418038736 | ||
|
|
3bf2b499a7 | ||
|
|
430da2b78d | ||
|
|
51047dcbce | ||
|
|
708bcaa630 | ||
|
|
724b20c826 | ||
|
|
69f8c46f15 | ||
|
|
b7f2fb0bdf | ||
|
|
222ffd8c53 | ||
|
|
00e0a0f79a | ||
|
|
d6bbc4383b | ||
|
|
b51a2602a9 | ||
|
|
c3fbddc671 | ||
|
|
ce262b691a | ||
|
|
65de6fef9b | ||
|
|
31d95ae5b0 | ||
|
|
429098733c | ||
|
|
597899d2bf | ||
|
|
b1c66b181d | ||
|
|
100b836979 | ||
|
|
817907c25a | ||
|
|
25195fffe2 | ||
|
|
0e784a6ffc | ||
|
|
351f96fa8b | ||
|
|
703fb4fb51 | ||
|
|
a975df6f8c | ||
|
|
fb90128b63 | ||
|
|
ea4c91fb37 | ||
|
|
7c1d6973ba | ||
|
|
4f2120b729 | ||
|
|
51f7d74e4a | ||
|
|
32a810fe1d | ||
|
|
570482521a | ||
|
|
3bd2a2f911 | ||
|
|
1ac6d086bd | ||
|
|
8374599183 | ||
|
|
0091c401df | ||
|
|
00dd67e345 | ||
|
|
158677bd54 | ||
|
|
a1b375316c | ||
|
|
dae2814b0f | ||
|
|
341a98848d | ||
|
|
902e66906a | ||
|
|
c59d3cd03f | ||
|
|
ed9f16e012 | ||
|
|
ed77a4aacd | ||
|
|
ff2fa70bd6 | ||
|
|
3519346df9 | ||
|
|
45f6caa7bd | ||
|
|
e2e7755af2 | ||
|
|
4ab7bfaa20 | ||
|
|
7e0ffb43a6 | ||
|
|
526c8328f6 | ||
|
|
81bad4eb25 | ||
|
|
dcaa6e81a3 | ||
|
|
e78155396d | ||
|
|
d3063c8117 | ||
|
|
cd1ed088c3 | ||
|
|
e831debb94 | ||
|
|
8d15a6612f | ||
|
|
9c034f3be6 | ||
|
|
60dd9349a8 | ||
|
|
330b1cbf7d | ||
|
|
9ffa38d54e | ||
|
|
c925727269 | ||
|
|
ac2dc06bd6 | ||
|
|
80b83adb53 | ||
|
|
ddc31ac2bb | ||
|
|
eb7c5d0b2b | ||
|
|
e5ce3cb62d | ||
|
|
7a69692999 | ||
|
|
d00d9d98dc | ||
|
|
1846c5c14c | ||
|
|
0d9db783f7 | ||
|
|
d3a6964d8f | ||
|
|
55e9cabf51 | ||
|
|
5180f2061a | ||
|
|
05154cf6aa | ||
|
|
f61e955945 | ||
|
|
d80605532f | ||
|
|
8d5fb14b50 | ||
|
|
570286c7df | ||
|
|
11e3ee8d90 | ||
|
|
0664c670f7 | ||
|
|
f36f9d412c | ||
|
|
bd734f1960 | ||
|
|
675a0b1a73 | ||
|
|
a2816277bf | ||
|
|
e37f1f914b | ||
|
|
adf7694308 | ||
|
|
c13449c6ad | ||
|
|
f72035210c | ||
|
|
89ef5238ea | ||
|
|
8198f66c1f | ||
|
|
eb5aa4ee44 | ||
|
|
adb09a0efe | ||
|
|
241eb8bed5 | ||
|
|
842a0d6ff3 | ||
|
|
3200917df2 | ||
|
|
12dcf0647d | ||
|
|
22859c9ffa | ||
|
|
29182156df | ||
|
|
357d63f398 | ||
|
|
e594bf208d | ||
|
|
2f0de3520a | ||
|
|
8def68eb02 | ||
|
|
aef33dc864 | ||
|
|
06d2f2d9e9 | ||
|
|
dad0ad76fb | ||
|
|
99fa8e7c8e | ||
|
|
ab0f175edf | ||
|
|
8cb9728565 | ||
|
|
d91d27f2a4 | ||
|
|
b2a6e038ae | ||
|
|
75f360903c | ||
|
|
79c3137b90 | ||
|
|
d2189d379c | ||
|
|
49aead7323 | ||
|
|
7bd610563f | ||
|
|
5c1a1ad162 | ||
|
|
86906d0049 | ||
|
|
93011dc754 | ||
|
|
b084b651af | ||
|
|
fecf9c0423 | ||
|
|
117fe78a4a | ||
|
|
153d43573a | ||
|
|
9145f5d2f8 | ||
|
|
835ea3b640 | ||
|
|
1e7eb23d1b | ||
|
|
cfe25d0bf5 | ||
|
|
b14939bd77 | ||
|
|
26e140ee22 | ||
|
|
1c5ecf8c01 | ||
|
|
9b528a05b5 | ||
|
|
3a7aa6d64f | ||
|
|
55820e47f9 |
19
.github/ISSUE_TEMPLATE/bug.md
vendored
19
.github/ISSUE_TEMPLATE/bug.md
vendored
@@ -5,14 +5,11 @@ labels: type:bug
|
||||
---
|
||||
|
||||
<!--
|
||||
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?
|
||||
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.
|
||||
-->
|
||||
|
||||
### Description
|
||||
@@ -21,9 +18,9 @@ Of course, we also expect you to search for existing similar issues first! ;) ht
|
||||
|
||||
### 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]
|
||||
* Drive: [Dokany/FUSE/WebDAV]
|
||||
* Volume type: [Dokany/FUSE/WebDAV, shown in the settings]
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
@@ -48,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.]
|
||||
|
||||
<!--
|
||||
|
||||
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.
|
||||
@@ -57,5 +53,4 @@ Log file location:
|
||||
- Windows: %appdata%/Cryptomator
|
||||
- macOS: ~/Library/Logs/Cryptomator
|
||||
- Linux: ~/.local/share/Cryptomator/logs
|
||||
|
||||
-->
|
||||
-->
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -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
|
||||
13
.github/ISSUE_TEMPLATE/feature.md
vendored
13
.github/ISSUE_TEMPLATE/feature.md
vendored
@@ -5,14 +5,9 @@ labels: type:feature-request
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Do you want to ask a QUESTION? Are you looking for SUPPORT?
|
||||
We're happy to help you via our support channels! Please read: https://github.com/cryptomator/cryptomator/blob/develop/SUPPORT.md
|
||||
|
||||
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! ;)
|
||||
|
||||
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=
|
||||
-->
|
||||
|
||||
### Summary
|
||||
@@ -29,4 +24,4 @@ Of course, we also expect you to search for existing similar feature requests fi
|
||||
|
||||
### Additional Context
|
||||
|
||||
[Add any other context or screenshots about the feature request here.]
|
||||
[Add any other context or screenshots about the feature request here.]
|
||||
|
||||
0
SECURITY.md → .github/SECURITY.md
vendored
0
SECURITY.md → .github/SECURITY.md
vendored
0
SUPPORT.md → .github/SUPPORT.md
vendored
0
SUPPORT.md → .github/SUPPORT.md
vendored
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -7,6 +7,7 @@ jobs:
|
||||
build:
|
||||
name: Build and Test
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
@@ -27,24 +28,27 @@ jobs:
|
||||
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
|
||||
$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 -jar ~/codacy-coverage-reporter.jar report -l Java -r main/keychain/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 -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
|
||||
env:
|
||||
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||
- name: Assemble Buildkit
|
||||
run: mvn -B package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease
|
||||
- name: Assemble buildkit-linux.zip
|
||||
run: mvn -B clean package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease,linux
|
||||
- name: Upload buildkit-linux.zip
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: buildkit-linux.zip
|
||||
path: main/buildkit/target/buildkit-linux.zip
|
||||
- name: Assemble buildkit-mac.zip
|
||||
run: mvn -B clean package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease,mac
|
||||
- name: Upload buildkit-mac.zip
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: buildkit-mac.zip
|
||||
path: main/buildkit/target/buildkit-mac.zip
|
||||
- name: Assemble buildkit-win.zip
|
||||
run: mvn -B clean package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease,windows
|
||||
- name: Upload buildkit-win.zip
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
|
||||
37
.github/workflows/triageBugs.yml
vendored
Normal file
37
.github/workflows/triageBugs.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Bug Report Triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
closeTemplateViolation:
|
||||
name: Validate bug report against issue template
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.issue.labels.*.name, 'type:bug')
|
||||
steps:
|
||||
- name: Check "Description"
|
||||
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: '### 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
|
||||
with:
|
||||
comment: |
|
||||
This bug report did ignore our issue template. 😞
|
||||
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._
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,5 +18,8 @@ pom.xml.versionsBackup
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/dictionaries
|
||||
.idea/compiler.xml
|
||||
.idea/encodings.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/**/libraries/
|
||||
*.iml
|
||||
29
.idea/codeStyles/Project.xml
generated
29
.idea/codeStyles/Project.xml
generated
@@ -9,14 +9,36 @@
|
||||
<option name="RIGHT_MARGIN" value="220" />
|
||||
<option name="FORMATTER_TAGS_ENABLED" value="true" />
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="30" />
|
||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
|
||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="10" />
|
||||
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
|
||||
<value />
|
||||
</option>
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<package name="javafx" withSubpackages="true" static="false" />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
|
||||
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
|
||||
</JavaCodeStyleSettings>
|
||||
<Properties>
|
||||
<option name="KEEP_BLANK_LINES" value="true" />
|
||||
</Properties>
|
||||
<XML>
|
||||
<option name="XML_ATTRIBUTE_WRAP" value="0" />
|
||||
</XML>
|
||||
<codeStyleSettings language="CSS">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Groovy">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
@@ -30,12 +52,17 @@
|
||||
<codeStyleSettings language="JAVA">
|
||||
<option name="KEEP_LINE_BREAKS" value="false" />
|
||||
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
|
||||
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
|
||||
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
|
||||
<option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
|
||||
<option name="ENUM_CONSTANTS_WRAP" value="2" />
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules />
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JSON">
|
||||
<indentOptions>
|
||||
|
||||
41
.idea/compiler.xml
generated
41
.idea/compiler.xml
generated
@@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile name="Annotation profile for Cryptomator" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<processorPath useClasspath="false">
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.27/dagger-compiler-2.27.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.27/dagger-2.27.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.27/dagger-producers-2.27.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/27.1-jre/guava-27.1-jre.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.1/jsr305-3.0.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-qual/2.5.2/checker-qual-2.5.2.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.2.0/error_prone_annotations-2.2.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.17/animal-sniffer-annotations-1.17.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.27/dagger-spi-2.27.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/net/ltgt/gradle/incap/incap/0.2/incap-0.2.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.3.50/kotlin-stdlib-1.3.50.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.50/kotlin-stdlib-common-1.3.50.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.1.0/kotlinx-metadata-jvm-0.1.0.jar" />
|
||||
</processorPath>
|
||||
<module name="keychain" />
|
||||
<module name="launcher" />
|
||||
<module name="commons" />
|
||||
<module name="ui" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
</project>
|
||||
13
.idea/encodings.xml
generated
13
.idea/encodings.xml
generated
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
|
||||
<file url="file://$PROJECT_DIR$/main" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/main/commons/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/main/keychain/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/main/keychain/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/main/launcher/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/main/ui/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/main/ui/src/main/resources" charset="UTF-8" />
|
||||
<file url="PROJECT" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
30
.idea/jarRepositories.xml
generated
30
.idea/jarRepositories.xml
generated
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="bintray" />
|
||||
<option name="name" value="Bintray JCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central Repository" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jcenter" />
|
||||
<option name="name" value="jcenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/runConfigurations/Cryptomator_Linux.xml
generated
2
.idea/runConfigurations/Cryptomator_Linux.xml
generated
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="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">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
10
.idea/runConfigurations/Cryptomator_Linux_Dev.xml
generated
Normal file
10
.idea/runConfigurations/Cryptomator_Linux_Dev.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<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" -Dcryptomator.showTrayIcon=true -Dfuse.experimental="true" -Xss20m -Xmx512m" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
2
.idea/runConfigurations/Cryptomator_Windows.xml
generated
2
.idea/runConfigurations/Cryptomator_Windows.xml
generated
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="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" -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">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
10
.idea/runConfigurations/Cryptomator_Windows_Dev.xml
generated
Normal file
10
.idea/runConfigurations/Cryptomator_Windows_Dev.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<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" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
3
.idea/runConfigurations/Cryptomator_macOS.xml
generated
3
.idea/runConfigurations/Cryptomator_macOS.xml
generated
@@ -5,8 +5,7 @@
|
||||
</envs>
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="launcher" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<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.mountPointsDir="/Volumes/" -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">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
13
.idea/runConfigurations/Cryptomator_macOS_Dev.xml
generated
Normal file
13
.idea/runConfigurations/Cryptomator_macOS_Dev.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Cryptomator macOS Dev" type="Application" factoryName="Application">
|
||||
<envs>
|
||||
<env name="LD_LIBRARY_PATH" value="/usr/local/lib" />
|
||||
</envs>
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<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" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -73,7 +73,9 @@ For more information on the security details visit [cryptomator.org](https://doc
|
||||
|
||||
```
|
||||
cd main
|
||||
mvn clean install -Prelease
|
||||
mvn clean install -Prelease,windows
|
||||
# or mvn clean install -Prelease,mac
|
||||
# or mvn clean install -Prelease,linux
|
||||
```
|
||||
|
||||
This will build all the jars and bundle them together with their OS-specific dependencies under `main/buildkit/target`. This can now be used to build native packages.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
<assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
|
||||
<id>tarball</id>
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
@@ -14,13 +14,6 @@
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target/</directory>
|
||||
<includes>
|
||||
<include>ffi-version.txt</include>
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target/</directory>
|
||||
<includes>
|
||||
@@ -43,12 +36,5 @@
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target/linux-libs</directory>
|
||||
<includes>
|
||||
<include>*.jar</include>
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
<assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
|
||||
<id>tarball</id>
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
@@ -14,13 +14,6 @@
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target/</directory>
|
||||
<includes>
|
||||
<include>ffi-version.txt</include>
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target/</directory>
|
||||
<includes>
|
||||
@@ -43,12 +36,5 @@
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target/mac-libs</directory>
|
||||
<includes>
|
||||
<include>*.jar</include>
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
<assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
|
||||
<id>tarball</id>
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
@@ -14,13 +14,6 @@
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target/</directory>
|
||||
<includes>
|
||||
<include>ffi-version.txt</include>
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target/</directory>
|
||||
<includes>
|
||||
@@ -43,12 +36,5 @@
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target/win-libs</directory>
|
||||
<includes>
|
||||
<include>*.jar</include>
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.5.2</version>
|
||||
<version>1.5.12</version>
|
||||
</parent>
|
||||
<artifactId>buildkit</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
@@ -24,7 +24,6 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-resources</id>
|
||||
@@ -55,8 +54,8 @@
|
||||
|
||||
<!-- copy libraries to target/libs/: -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>3.1.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
@@ -65,110 +64,153 @@
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<includeScope>runtime</includeScope>
|
||||
<outputDirectory>${project.build.directory}/libs</outputDirectory>
|
||||
<excludeClassifiers>linux,mac,win</excludeClassifiers>
|
||||
<excludeArtifactIds>dbus-java,secret-service,hkdf,java-utils</excludeArtifactIds>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>copy-linux-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/linux-libs</outputDirectory>
|
||||
<includeGroupIds>org.openjfx</includeGroupIds>
|
||||
<classifier>linux</classifier>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>copy-linux-secret-service</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/linux-libs</outputDirectory>
|
||||
<includeArtifactIds>dbus-java,secret-service,hkdf,java-utils</includeArtifactIds>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>copy-mac-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/mac-libs</outputDirectory>
|
||||
<includeGroupIds>org.openjfx</includeGroupIds>
|
||||
<classifier>mac</classifier>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>copy-win-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/win-libs</outputDirectory>
|
||||
<includeGroupIds>org.openjfx</includeGroupIds>
|
||||
<classifier>win</classifier>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- create buildkit.zip: -->
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.1.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>assemble-linux</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly-linux.xml</descriptor>
|
||||
</descriptors>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<finalName>buildkit-linux</finalName>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>assemble-mac</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly-mac.xml</descriptor>
|
||||
</descriptors>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<finalName>buildkit-mac</finalName>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>assemble-win</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly-win.xml</descriptor>
|
||||
</descriptors>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<finalName>buildkit-win</finalName>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>linux</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>assemble-linux</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly-linux.xml</descriptor>
|
||||
</descriptors>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<finalName>buildkit-linux</finalName>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-linux-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/libs</outputDirectory>
|
||||
<includeGroupIds>org.openjfx</includeGroupIds>
|
||||
<classifier>linux</classifier>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>mac</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>assemble-mac</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly-mac.xml</descriptor>
|
||||
</descriptors>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<finalName>buildkit-mac</finalName>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-mac-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/libs</outputDirectory>
|
||||
<includeGroupIds>org.openjfx</includeGroupIds>
|
||||
<classifier>mac</classifier>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>windows</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>assemble-win</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly-win.xml</descriptor>
|
||||
</descriptors>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<finalName>buildkit-win</finalName>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-win-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/libs</outputDirectory>
|
||||
<includeGroupIds>org.openjfx</includeGroupIds>
|
||||
<classifier>win</classifier>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -1 +0,0 @@
|
||||
${cryptomator.jni.version}
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/sh
|
||||
cd $(dirname $0)
|
||||
java \
|
||||
-cp "libs/*" \
|
||||
-Dcryptomator.settingsPath="~/.config/Cryptomator/settings.json" \
|
||||
@@ -6,6 +7,6 @@ java \
|
||||
-Dcryptomator.logDir="~/.local/share/Cryptomator/logs" \
|
||||
-Dcryptomator.mountPointsDir="~/.local/share/Cryptomator/mnt" \
|
||||
-Djdk.gtk.version=2 \
|
||||
-Xss20m \
|
||||
-Xss2m \
|
||||
-Xmx512m \
|
||||
org.cryptomator.launcher.Cryptomator
|
||||
org.cryptomator.launcher.Cryptomator
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/sh
|
||||
cd $(dirname $0)
|
||||
java \
|
||||
-cp "libs/*" \
|
||||
-Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" \
|
||||
|
||||
@@ -4,6 +4,7 @@ java ^
|
||||
-Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" ^
|
||||
-Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator/ipcPort.bin" ^
|
||||
-Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" ^
|
||||
-Dcryptomator.mountPointsDir="~/Cryptomator" ^
|
||||
-Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" ^
|
||||
-Xss20m ^
|
||||
-Xmx512m ^
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.5.2</version>
|
||||
<version>1.5.12</version>
|
||||
</parent>
|
||||
<artifactId>commons</artifactId>
|
||||
<name>Cryptomator Commons</name>
|
||||
@@ -29,7 +29,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>jni</artifactId>
|
||||
<artifactId>integrations-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JavaFx -->
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<!-- EasyBind -->
|
||||
<dependency>
|
||||
<groupId>org.fxmisc.easybind</groupId>
|
||||
<groupId>com.tobiasdiez</groupId>
|
||||
<artifactId>easybind</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -5,24 +5,25 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.keychain.KeychainModule;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.SettingsProvider;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultComponent;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -33,7 +34,7 @@ import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Module(subcomponents = {VaultComponent.class})
|
||||
@Module(subcomponents = {VaultComponent.class}, includes = {KeychainModule.class})
|
||||
public abstract class CommonsModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
|
||||
|
||||
@@ -21,14 +21,13 @@ import java.util.stream.StreamSupport;
|
||||
public class Environment {
|
||||
|
||||
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 ABSOLUTE_HOME_DIR = Paths.get(USER_HOME);
|
||||
private static final char PATH_LIST_SEP = ':';
|
||||
private static final int DEFAULT_MIN_PW_LENGTH = 8;
|
||||
|
||||
@Inject
|
||||
public Environment() {
|
||||
LOG.debug("user.home: {}", System.getProperty("user.home"));
|
||||
LOG.debug("java.library.path: {}", System.getProperty("java.library.path"));
|
||||
LOG.debug("user.language: {}", System.getProperty("user.language"));
|
||||
LOG.debug("user.region: {}", System.getProperty("user.region"));
|
||||
@@ -40,6 +39,8 @@ public class Environment {
|
||||
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
|
||||
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
|
||||
LOG.debug("cryptomator.buildNumber: {}", System.getProperty("cryptomator.buildNumber"));
|
||||
LOG.debug("cryptomator.showTrayIcon: {}", System.getProperty("cryptomator.showTrayIcon"));
|
||||
LOG.debug("fuse.experimental: {}", Boolean.getBoolean("fuse.experimental"));
|
||||
}
|
||||
|
||||
public boolean useCustomLogbackConfig() {
|
||||
@@ -74,6 +75,15 @@ public class Environment {
|
||||
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() {
|
||||
return Boolean.getBoolean("fuse.experimental");
|
||||
}
|
||||
|
||||
private int getInt(String propertyName, int defaultValue) {
|
||||
String value = System.getProperty(propertyName);
|
||||
try {
|
||||
@@ -87,8 +97,13 @@ public class Environment {
|
||||
String value = System.getProperty(propertyName);
|
||||
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<String> rawSettingsPaths = getRawList(propertyName, PATH_LIST_SEP);
|
||||
return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Paths::get).map(this::replaceHomeDir);
|
||||
@@ -96,7 +111,7 @@ public class Environment {
|
||||
|
||||
private Path replaceHomeDir(Path path) {
|
||||
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 {
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2019 Skymatic GmbH.
|
||||
* 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 dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.cryptomator.jni.JniFunctions;
|
||||
import org.cryptomator.jni.MacFunctions;
|
||||
import org.cryptomator.jni.WinFunctions;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Optional;
|
||||
|
||||
@Module
|
||||
public class JniModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
Optional<MacFunctions> provideOptionalMacFunctions() {
|
||||
return JniFunctions.macFunctions();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
Optional<WinFunctions> provideOptionalWinFunctions() {
|
||||
return JniFunctions.winFunctions();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Compares version strings according to <a href="http://semver.org/spec/v2.0.0.html">SemVer 2.0.0</a>.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package org.cryptomator.common.keychain;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import java.util.Arrays;
|
||||
|
||||
@Singleton
|
||||
public class KeychainManager implements KeychainAccessProvider {
|
||||
|
||||
private final ObjectExpression<KeychainAccessProvider> keychain;
|
||||
private final LoadingCache<String, BooleanProperty> passphraseStoredProperties;
|
||||
|
||||
@Inject
|
||||
KeychainManager(ObjectExpression<KeychainAccessProvider> selectedKeychain) {
|
||||
this.keychain = selectedKeychain;
|
||||
this.passphraseStoredProperties = CacheBuilder.newBuilder() //
|
||||
.weakValues() //
|
||||
.build(CacheLoader.from(this::createStoredPassphraseProperty));
|
||||
keychain.addListener(ignored -> passphraseStoredProperties.invalidateAll());
|
||||
}
|
||||
|
||||
private KeychainAccessProvider getKeychainOrFail() throws KeychainAccessException {
|
||||
var result = keychain.getValue();
|
||||
if (result == null) {
|
||||
throw new NoKeychainAccessProviderException();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
|
||||
getKeychainOrFail().storePassphrase(key, passphrase);
|
||||
setPassphraseStored(key, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] loadPassphrase(String key) throws KeychainAccessException {
|
||||
char[] passphrase = getKeychainOrFail().loadPassphrase(key);
|
||||
setPassphraseStored(key, passphrase != null);
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePassphrase(String key) throws KeychainAccessException {
|
||||
getKeychainOrFail().deletePassphrase(key);
|
||||
setPassphraseStored(key, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
|
||||
getKeychainOrFail().changePassphrase(key, passphrase);
|
||||
setPassphraseStored(key, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return keychain.getValue() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the keychain knows a passphrase for the given key.
|
||||
* <p>
|
||||
* Expensive operation. If possible, use {@link #getPassphraseStoredProperty(String)} instead.
|
||||
*
|
||||
* @param key The key to look up
|
||||
* @return <code>true</code> if a password for <code>key</code> is stored.
|
||||
* @throws KeychainAccessException
|
||||
*/
|
||||
public boolean isPassphraseStored(String key) throws KeychainAccessException {
|
||||
char[] storedPw = null;
|
||||
try {
|
||||
storedPw = getKeychainOrFail().loadPassphrase(key);
|
||||
return storedPw != null;
|
||||
} finally {
|
||||
if (storedPw != null) {
|
||||
Arrays.fill(storedPw, ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setPassphraseStored(String key, boolean value) {
|
||||
BooleanProperty property = passphraseStoredProperties.getIfPresent(key);
|
||||
if (property != null) {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
property.set(value);
|
||||
} else {
|
||||
Platform.runLater(() -> property.set(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable property for use in the UI that tells whether a passphrase is stored for the given key.
|
||||
* <p>
|
||||
* Assuming that this process is the only process modifying Cryptomator-related items in the system keychain, this
|
||||
* property stays in memory in an attempt to avoid unnecessary calls to the system keychain. Note that due to this
|
||||
* fact the value stored in the returned property is not 100% reliable. Code defensively!
|
||||
*
|
||||
* @param key The key to look up
|
||||
* @return An observable property which is <code>true</code> when it almost certain that a password for <code>key</code> is stored.
|
||||
* @see #isPassphraseStored(String)
|
||||
*/
|
||||
public ReadOnlyBooleanProperty getPassphraseStoredProperty(String key) {
|
||||
return passphraseStoredProperties.getUnchecked(key);
|
||||
}
|
||||
|
||||
private BooleanProperty createStoredPassphraseProperty(String key) {
|
||||
try {
|
||||
return new SimpleBooleanProperty(isPassphraseStored(key));
|
||||
} catch (KeychainAccessException e) {
|
||||
return new SimpleBooleanProperty(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.cryptomator.common.keychain;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Module
|
||||
public class KeychainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Set<ServiceLoader.Provider<KeychainAccessProvider>> provideAvailableKeychainAccessProviderFactories() {
|
||||
return ServiceLoader.load(KeychainAccessProvider.class).stream().collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Set<KeychainAccessProvider> provideSupportedKeychainAccessProviders(Set<ServiceLoader.Provider<KeychainAccessProvider>> availableFactories) {
|
||||
return availableFactories.stream() //
|
||||
.map(ServiceLoader.Provider::get) //
|
||||
.filter(KeychainAccessProvider::isSupported) //
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static ObjectExpression<KeychainAccessProvider> provideKeychainAccessProvider(Settings settings, Set<KeychainAccessProvider> providers) {
|
||||
return Bindings.createObjectBinding(() -> {
|
||||
var selectedProviderClass = settings.keychainBackend().get().getProviderClass();
|
||||
var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny();
|
||||
var fallbackProvider = providers.stream().findAny().orElse(null);
|
||||
return selectedProvider.orElse(fallbackProvider);
|
||||
}, settings.keychainBackend());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.common.keychain;
|
||||
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
|
||||
/**
|
||||
* Thrown by {@link KeychainManager} if attempted to access a keychain despite no supported keychain access provider being available.
|
||||
*/
|
||||
public class NoKeychainAccessProviderException extends KeychainAccessException {
|
||||
|
||||
public NoKeychainAccessProviderException() {
|
||||
super("Did not find any supported keychain access provider.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.common.vaults.WindowsDriveLetters;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
class AvailableDriveLetterChooser implements MountPointChooser {
|
||||
|
||||
private final WindowsDriveLetters windowsDriveLetters;
|
||||
|
||||
@Inject
|
||||
public AvailableDriveLetterChooser(WindowsDriveLetters windowsDriveLetters) {
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
return SystemUtils.IS_OS_WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
return this.windowsDriveLetters.getAvailableDriveLetterPath();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
|
||||
class CustomDriveLetterChooser implements MountPointChooser {
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
|
||||
@Inject
|
||||
public CustomDriveLetterChooser(VaultSettings vaultSettings) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
return SystemUtils.IS_OS_WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
return this.vaultSettings.getWinDriveLetter().map(letter -> letter.charAt(0) + ":\\").map(Paths::get);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
|
||||
class CustomMountPointChooser implements MountPointChooser {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class);
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Environment environment;
|
||||
|
||||
@Inject
|
||||
public CustomMountPointChooser(VaultSettings vaultSettings, Environment environment) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
//Disable if useExperimentalFuse is required (Win + Fuse), but set to false
|
||||
return caller.getImplementationType() != VolumeImpl.FUSE || !SystemUtils.IS_OS_WINDOWS || environment.useExperimentalFuse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
//VaultSettings#getCustomMountPath already checks whether the saved custom mountpoint should be used
|
||||
return this.vaultSettings.getCustomMountPath().map(Paths::get);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
||||
switch (caller.getMountPointRequirement()) {
|
||||
case PARENT_NO_MOUNT_POINT -> prepareParentNoMountPoint(mountPoint);
|
||||
case EMPTY_MOUNT_POINT -> prepareEmptyMountPoint(mountPoint);
|
||||
case NONE -> {
|
||||
//Requirement "NONE" doesn't make any sense here.
|
||||
//No need to prepare/verify a Mountpoint without requiring one...
|
||||
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
|
||||
}
|
||||
default -> {
|
||||
//Currently the case for "PARENT_OPT_MOUNT_POINT"
|
||||
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
|
||||
}
|
||||
}
|
||||
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException {
|
||||
//This the case on Windows when using FUSE
|
||||
//See https://github.com/billziss-gh/winfsp/issues/320
|
||||
Path parent = mountPoint.getParent();
|
||||
if (!Files.isDirectory(parent)) {
|
||||
throw new InvalidMountPointException(new NotDirectoryException(parent.toString()));
|
||||
}
|
||||
//We must use #notExists() here because notExists =/= !exists (see docs)
|
||||
if (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
|
||||
//File exists OR can't be determined
|
||||
throw new InvalidMountPointException(new FileAlreadyExistsException(mountPoint.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException {
|
||||
//This is the case for Windows when using Dokany and for Linux and Mac
|
||||
if (!Files.isDirectory(mountPoint)) {
|
||||
throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString()));
|
||||
}
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(mountPoint)) {
|
||||
if (ds.iterator().hasNext()) {
|
||||
throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString()));
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new InvalidMountPointException("IOException while checking folder content", exception);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
public class InvalidMountPointException extends Exception {
|
||||
|
||||
public InvalidMountPointException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidMountPointException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public InvalidMountPointException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
class MacVolumeMountChooser implements MountPointChooser {
|
||||
|
||||
private static final Path VOLUME_PATH = Path.of("/Volumes");
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final MountPointHelper helper;
|
||||
|
||||
@Inject
|
||||
public MacVolumeMountChooser(VaultSettings vaultSettings, MountPointHelper helper) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.helper = helper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
return SystemUtils.IS_OS_MAC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
return Optional.of(helper.chooseTemporaryMountPoint(vaultSettings, VOLUME_PATH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare(Volume caller, Path mountPoint) {
|
||||
// https://github.com/osxfuse/osxfuse/issues/306#issuecomment-245114592:
|
||||
// In order to allow non-admin users to mount FUSE volumes in `/Volumes`,
|
||||
// starting with version 3.5.0, FUSE will create non-existent mount points automatically.
|
||||
// Therefore we don't need to prepare anything.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.SortedSet;
|
||||
|
||||
/**
|
||||
* Base interface for the Mountpoint-Choosing-Operation that results in the choice and
|
||||
* preparation of a mountpoint or an exception otherwise.<br>
|
||||
* <p>All <i>MountPointChoosers (MPCs)</i> need to implement this class and must be added to
|
||||
* the pool of possible MPCs by the {@link MountPointChooserModule MountPointChooserModule.}
|
||||
* The MountPointChooserModule will sort them according to their {@link #getPriority() priority.}
|
||||
* The priority must be defined by the developer to reflect a useful execution order.<br>
|
||||
* A specific priority <b>must not</b> be assigned to more than one MPC at a time;
|
||||
* the result of having two MPCs with equal priority is undefined.
|
||||
*
|
||||
* <p>MPCs are executed by a {@link Volume} in ascending order of their priority
|
||||
* (smaller priorities are tried first) to find and prepare a suitable mountpoint for the volume.
|
||||
* The volume has access to a {@link SortedSet} of MPCs in this specific order,
|
||||
* that is provided by the Module. The Set contains all available Choosers, even if they
|
||||
* are not {@link #isApplicable(Volume) applicable} for the Vault/Volume. The Volume must
|
||||
* check whether a MPC is applicable by invoking {@code #isApplicable(Volume)} on it
|
||||
* <i>before</i> calling {@code #chooseMountPoint(Volume)}.
|
||||
*
|
||||
* <p>At execution of a MPC {@link #chooseMountPoint(Volume)} is called to choose a mountpoint
|
||||
* according to the MPC's <i>strategy.</i> The <i>strategy</i> can involve reading configs,
|
||||
* searching the filesystem, processing user-input or similar operations.
|
||||
* If {@code #chooseMountPoint(Volume)} returns a non-null path (everything but
|
||||
* {@linkplain Optional#empty()}) the MPC's {@link #prepare(Volume, Path)} method is called and the
|
||||
* MountPoint is verified and/or prepared. In this case <i>no other MPC's will be called for
|
||||
* this volume, even if {@code #prepare(Volume, Path)} fails.</i>
|
||||
*
|
||||
* <p>If {@code #chooseMountPoint(Volume)} yields no result, the next MPC is executed
|
||||
* <i>without</i> first calling the {@code #prepare(Volume, Path)} method of the current MPC.
|
||||
* This is repeated until<br>
|
||||
* <ul>
|
||||
* <li><b>either</b> a mountpoint is returned by {@code #chooseMountPoint(Volume)}
|
||||
* and {@code #prepare(Volume, Path)} succeeds or fails, ending the entire operation</li>
|
||||
* <li><b>or</b> no MPC remains and an {@link InvalidMountPointException} is thrown.</li>
|
||||
* </ul>
|
||||
* If the {@code #prepare(Volume, Path)} method of a MPC fails, the entire
|
||||
* Mountpoint-Choosing-Operation is aborted and the method should do all necessary cleanup
|
||||
* before throwing the exception.
|
||||
* If the preparation succeeds {@link #cleanup(Volume, Path)} can be used after unmount to do any
|
||||
* remaining cleanup.
|
||||
*/
|
||||
public interface MountPointChooser {
|
||||
|
||||
/**
|
||||
* Called by the {@link Volume} to determine whether this MountPointChooser is
|
||||
* applicable for mounting the Vault/Volume, especially with regard to the
|
||||
* current system configuration and particularities of the Volume type.
|
||||
*
|
||||
* <p>Developers should override this method to check for system configurations
|
||||
* that are unsuitable for this MPC.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to determine applicability of the MPC
|
||||
* @return a boolean flag; true if applicable, else false.
|
||||
* @see #chooseMountPoint(Volume)
|
||||
*/
|
||||
boolean isApplicable(Volume caller);
|
||||
|
||||
/**
|
||||
* Called by a {@link Volume} to choose a mountpoint according to the
|
||||
* MountPointChoosers strategy.
|
||||
*
|
||||
* <p>This method must only be called for MPCs that were deemed
|
||||
* {@link #isApplicable(Volume) applicable} by the {@link Volume Volume.}
|
||||
* Developers should override this method to find or extract a mountpoint for
|
||||
* the volume <b>without</b> preparing it. Preparation should be done by
|
||||
* {@link #prepare(Volume, Path)} instead.
|
||||
* Exceptions in this method should be handled gracefully and result in returning
|
||||
* {@link Optional#empty()} instead of throwing an exception.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to choose a mountpoint
|
||||
* @return the chosen path or {@link Optional#empty()} if an exception occurred
|
||||
* or no mountpoint could be found.
|
||||
* @see #isApplicable(Volume)
|
||||
* @see #prepare(Volume, Path)
|
||||
*/
|
||||
Optional<Path> chooseMountPoint(Volume caller);
|
||||
|
||||
/**
|
||||
* Called by a {@link Volume} to prepare and/or verify the chosen mountpoint.<br>
|
||||
* This method is only called if the {@link #chooseMountPoint(Volume)} method
|
||||
* of the same MountPointChooser returned a path.
|
||||
*
|
||||
* <p>Developers should override this method to prepare the mountpoint for
|
||||
* the volume and check for any obstacles that could hinder the mount operation.
|
||||
* The mountpoint is deemed "prepared" if it can be used to mount a volume
|
||||
* without any further filesystem actions or user interaction. If this is not possible,
|
||||
* this method should fail. In other words: This method should not return without
|
||||
* either failing or finalizing the preparation of the mountpoint.
|
||||
* Generally speaking exceptions should be wrapped as
|
||||
* {@link InvalidMountPointException} to allow efficient handling by the caller.
|
||||
*
|
||||
* <p>Often the preparation of a mountpoint involves creating files or others
|
||||
* actions that require cleaning up after the volume is unmounted.
|
||||
* In this case developers should override the {@link #cleanup(Volume, Path)}
|
||||
* method and return {@code true} to the volume to indicate that the
|
||||
* {@code #cleanup} method of this MPC should be called after unmount.
|
||||
*
|
||||
* <p><b>Please note:</b> If this method fails the entire
|
||||
* Mountpoint-Choosing-Operation is aborted without calling
|
||||
* {@link #cleanup(Volume, Path)} or any other MPCs. Therefore this method should
|
||||
* do all necessary cleanup before throwing the exception.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to prepare a mountpoint
|
||||
* @param mountPoint the mountpoint chosen by {@link #chooseMountPoint(Volume)}
|
||||
* @return a boolean flag; true if cleanup is needed, false otherwise
|
||||
* @throws InvalidMountPointException if the preparation fails
|
||||
* @see #chooseMountPoint(Volume)
|
||||
* @see #cleanup(Volume, Path)
|
||||
*/
|
||||
default boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
||||
return false; //NO-OP
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by a {@link Volume} to do any cleanup needed after unmount.
|
||||
*
|
||||
* <p>This method is only called if the {@link #prepare(Volume, Path)} method
|
||||
* of the same MountPointChooser returned {@code true}. Typically developers want to
|
||||
* delete any files created prior to mount or do similar tasks.<br>
|
||||
* Exceptions in this method should be handled gracefully.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to cleanup the prepared mountpoint
|
||||
* @param mountPoint the mountpoint that was prepared by {@link #prepare(Volume, Path)}
|
||||
* @see #prepare(Volume, Path)
|
||||
*/
|
||||
default void cleanup(Volume caller, Path mountPoint) {
|
||||
//NO-OP
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntKey;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.common.vaults.PerVault;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Dagger-Module for {@link MountPointChooser MountPointChoosers.}<br>
|
||||
* See there for additional information.
|
||||
*
|
||||
* @see MountPointChooser
|
||||
*/
|
||||
@Module
|
||||
public abstract class MountPointChooserModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(0)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(100)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(101)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindMacVolumeMountChooser(MacVolumeMountChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(200)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(999)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindTemporaryMountPointChooser(TemporaryMountPointChooser chooser);
|
||||
|
||||
@Provides
|
||||
@PerVault
|
||||
@Named("orderedMountPointChoosers")
|
||||
public static Iterable<MountPointChooser> provideOrderedMountPointChoosers(Map<Integer, MountPointChooser> choosers) {
|
||||
SortedMap<Integer, MountPointChooser> sortedChoosers = new TreeMap<>(choosers);
|
||||
return Iterables.unmodifiableIterable(sortedChoosers.values());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
class MountPointHelper {
|
||||
|
||||
public static Logger LOG = LoggerFactory.getLogger(MountPointHelper.class);
|
||||
private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
|
||||
|
||||
private final Optional<Path> tmpMountPointDir;
|
||||
private volatile boolean unmountDebrisCleared = false;
|
||||
|
||||
@Inject
|
||||
public MountPointHelper(Environment env) {
|
||||
this.tmpMountPointDir = env.getMountPointsDir();
|
||||
}
|
||||
|
||||
public Path chooseTemporaryMountPoint(VaultSettings vaultSettings, Path parentDir) {
|
||||
String basename = vaultSettings.mountName().get();
|
||||
//regular
|
||||
Path mountPoint = parentDir.resolve(basename);
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
//with id
|
||||
mountPoint = parentDir.resolve(basename + " (" + vaultSettings.getId() + ")");
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
//with id and count
|
||||
for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
|
||||
mountPoint = parentDir.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
}
|
||||
LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parentDir, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized void clearIrregularUnmountDebrisIfNeeded() {
|
||||
if (unmountDebrisCleared || tmpMountPointDir.isEmpty()) {
|
||||
return; // nothing to do
|
||||
}
|
||||
if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) {
|
||||
clearIrregularUnmountDebris(tmpMountPointDir.get());
|
||||
}
|
||||
unmountDebrisCleared = true;
|
||||
}
|
||||
|
||||
private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {
|
||||
IOException cleanupFailed = new IOException("Cleanup failed");
|
||||
|
||||
try {
|
||||
LOG.debug("Performing cleanup of mountpoint dir {}.", dirContainingMountPoints);
|
||||
for (Path p : Files.newDirectoryStream(dirContainingMountPoints)) {
|
||||
try {
|
||||
var attr = Files.readAttributes(p, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
|
||||
if (attr.isOther() && attr.isDirectory()) { // yes, this is possible with windows junction points -.-
|
||||
Files.delete(p);
|
||||
} else if (attr.isDirectory()) {
|
||||
deleteEmptyDir(p);
|
||||
} else if (attr.isSymbolicLink()) {
|
||||
deleteDeadLink(p);
|
||||
} else {
|
||||
LOG.debug("Found non-directory element in mountpoint dir: {}", p);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
cleanupFailed.addSuppressed(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanupFailed.getSuppressed().length > 0) {
|
||||
throw cleanupFailed;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e);
|
||||
} finally {
|
||||
unmountDebrisCleared = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteEmptyDir(Path dir) throws IOException {
|
||||
assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
|
||||
try {
|
||||
ensureIsEmpty(dir);
|
||||
Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents)
|
||||
} catch (DirectoryNotEmptyException e) {
|
||||
LOG.info("Found non-empty directory in mountpoint dir: {}", dir);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteDeadLink(Path symlink) throws IOException {
|
||||
assert Files.isSymbolicLink(symlink);
|
||||
if (Files.notExists(symlink)) { // following link: target does not exist
|
||||
Files.delete(symlink);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureIsEmpty(Path dir) throws IOException {
|
||||
if (Files.newDirectoryStream(dir).iterator().hasNext()) {
|
||||
throw new DirectoryNotEmptyException(dir.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
class TemporaryMountPointChooser implements MountPointChooser {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Environment environment;
|
||||
private final MountPointHelper helper;
|
||||
|
||||
@Inject
|
||||
public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, MountPointHelper helper) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.environment = environment;
|
||||
this.helper = helper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
if (this.environment.getMountPointsDir().isEmpty()) {
|
||||
LOG.warn("\"cryptomator.mountPointsDir\" is not set to a valid path!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
assert environment.getMountPointsDir().isPresent();
|
||||
//clean leftovers of not-regularly unmounted vaults
|
||||
//see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061
|
||||
helper.clearIrregularUnmountDebrisIfNeeded();
|
||||
return this.environment.getMountPointsDir().map(dir -> this.helper.chooseTemporaryMountPoint(this.vaultSettings, dir));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
||||
try {
|
||||
switch (caller.getMountPointRequirement()) {
|
||||
case PARENT_NO_MOUNT_POINT -> {
|
||||
Files.createDirectories(mountPoint.getParent());
|
||||
LOG.debug("Successfully created folder for mount point: {}", mountPoint);
|
||||
return false;
|
||||
}
|
||||
case EMPTY_MOUNT_POINT -> {
|
||||
Files.createDirectories(mountPoint);
|
||||
LOG.debug("Successfully created mount point: {}", mountPoint);
|
||||
return true;
|
||||
}
|
||||
case NONE -> {
|
||||
//Requirement "NONE" doesn't make any sense here.
|
||||
//No need to prepare/verify a Mountpoint without requiring one...
|
||||
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
|
||||
}
|
||||
default -> {
|
||||
//Currently the case for "PARENT_OPT_MOUNT_POINT"
|
||||
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
|
||||
}
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new InvalidMountPointException("IOException while preparing mountpoint", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Volume caller, Path mountPoint) {
|
||||
try {
|
||||
Files.delete(mountPoint);
|
||||
LOG.debug("Successfully deleted mount point: {}", mountPoint);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Could not delete mount point: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
public enum KeychainBackend {
|
||||
GNOME("org.cryptomator.linux.keychain.SecretServiceKeychainAccess"),
|
||||
KDE("org.cryptomator.linux.keychain.KDEWalletKeychainAccess"),
|
||||
MAC_SYSTEM_KEYCHAIN("org.cryptomator.macos.keychain.MacSystemKeychainAccess"),
|
||||
WIN_SYSTEM_KEYCHAIN("org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess");
|
||||
|
||||
private final String providerClass;
|
||||
|
||||
KeychainBackend(String providerClass) {
|
||||
this.providerClass = providerClass;
|
||||
}
|
||||
|
||||
public String getProviderClass() {
|
||||
return providerClass;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
@@ -20,7 +23,6 @@ import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.NodeOrientation;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class Settings {
|
||||
@@ -34,10 +36,12 @@ public class Settings {
|
||||
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
|
||||
public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV;
|
||||
public static final boolean DEFAULT_DEBUG_MODE = false;
|
||||
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
|
||||
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = SystemUtils.IS_OS_WINDOWS ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
|
||||
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 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 BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
|
||||
@@ -49,15 +53,20 @@ public class Settings {
|
||||
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
|
||||
private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
|
||||
private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
|
||||
private final ObjectProperty<KeychainBackend> keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND);
|
||||
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
|
||||
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;
|
||||
|
||||
/**
|
||||
* Package-private constructor; use {@link SettingsProvider}.
|
||||
*/
|
||||
Settings() {
|
||||
Settings(Environment env) {
|
||||
this.showTrayIcon = new SimpleBooleanProperty(env.showTrayIcon());
|
||||
|
||||
directories.addListener(this::somethingChanged);
|
||||
askedForUpdateCheck.addListener(this::somethingChanged);
|
||||
checkForUpdates.addListener(this::somethingChanged);
|
||||
@@ -68,14 +77,17 @@ public class Settings {
|
||||
debugMode.addListener(this::somethingChanged);
|
||||
preferredVolumeImpl.addListener(this::somethingChanged);
|
||||
theme.addListener(this::somethingChanged);
|
||||
keychainBackend.addListener(this::somethingChanged);
|
||||
userInterfaceOrientation.addListener(this::somethingChanged);
|
||||
licenseKey.addListener(this::somethingChanged);
|
||||
showMinimizeButton.addListener(this::somethingChanged);
|
||||
showTrayIcon.addListener(this::somethingChanged);
|
||||
}
|
||||
|
||||
void setSaveCmd(Consumer<Settings> saveCmd) {
|
||||
this.saveCmd = saveCmd;
|
||||
}
|
||||
|
||||
|
||||
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
this.save();
|
||||
}
|
||||
@@ -99,7 +111,7 @@ public class Settings {
|
||||
public BooleanProperty checkForUpdates() {
|
||||
return checkForUpdates;
|
||||
}
|
||||
|
||||
|
||||
public BooleanProperty startHidden() {
|
||||
return startHidden;
|
||||
}
|
||||
@@ -128,6 +140,8 @@ public class Settings {
|
||||
return theme;
|
||||
}
|
||||
|
||||
public ObjectProperty<KeychainBackend> keychainBackend() { return keychainBackend; }
|
||||
|
||||
public ObjectProperty<NodeOrientation> userInterfaceOrientation() {
|
||||
return userInterfaceOrientation;
|
||||
}
|
||||
@@ -135,4 +149,12 @@ public class Settings {
|
||||
public StringProperty licenseKey() {
|
||||
return licenseKey;
|
||||
}
|
||||
|
||||
public BooleanProperty showMinimizeButton() {
|
||||
return showMinimizeButton;
|
||||
}
|
||||
|
||||
public BooleanProperty showTrayIcon() {
|
||||
return showTrayIcon;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,29 @@ import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import javafx.geometry.NodeOrientation;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.geometry.NodeOrientation;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Singleton
|
||||
public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class);
|
||||
|
||||
private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter();
|
||||
private final Environment env;
|
||||
|
||||
@Inject
|
||||
public SettingsJsonAdapter(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter out, Settings value) throws IOException {
|
||||
@@ -38,7 +48,10 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
|
||||
out.name("theme").value(value.theme().get().name());
|
||||
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
|
||||
out.name("keychainBackend").value(value.keychainBackend().get().name());
|
||||
out.name("licenseKey").value(value.licenseKey().get());
|
||||
out.name("showMinimizeButton").value(value.showMinimizeButton().get());
|
||||
out.name("showTrayIcon").value(value.showTrayIcon().get());
|
||||
out.endObject();
|
||||
}
|
||||
|
||||
@@ -52,52 +65,31 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
|
||||
@Override
|
||||
public Settings read(JsonReader in) throws IOException {
|
||||
Settings settings = new Settings();
|
||||
Settings settings = new Settings(env);
|
||||
|
||||
in.beginObject();
|
||||
while (in.hasNext()) {
|
||||
String name = in.nextName();
|
||||
switch (name) {
|
||||
case "directories":
|
||||
settings.getDirectories().addAll(readVaultSettingsArray(in));
|
||||
break;
|
||||
case "askedForUpdateCheck":
|
||||
settings.askedForUpdateCheck().set(in.nextBoolean());
|
||||
break;
|
||||
case "checkForUpdatesEnabled":
|
||||
settings.checkForUpdates().set(in.nextBoolean());
|
||||
break;
|
||||
case "startHidden":
|
||||
settings.startHidden().set(in.nextBoolean());
|
||||
break;
|
||||
case "port":
|
||||
settings.port().set(in.nextInt());
|
||||
break;
|
||||
case "numTrayNotifications":
|
||||
settings.numTrayNotifications().set(in.nextInt());
|
||||
break;
|
||||
case "preferredGvfsScheme":
|
||||
settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
|
||||
break;
|
||||
case "debugMode":
|
||||
settings.debugMode().set(in.nextBoolean());
|
||||
break;
|
||||
case "preferredVolumeImpl":
|
||||
settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
|
||||
break;
|
||||
case "theme":
|
||||
settings.theme().set(parseUiTheme(in.nextString()));
|
||||
break;
|
||||
case "uiOrientation":
|
||||
settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
|
||||
break;
|
||||
case "licenseKey":
|
||||
settings.licenseKey().set(in.nextString());
|
||||
break;
|
||||
default:
|
||||
case "directories" -> settings.getDirectories().addAll(readVaultSettingsArray(in));
|
||||
case "askedForUpdateCheck" -> settings.askedForUpdateCheck().set(in.nextBoolean());
|
||||
case "checkForUpdatesEnabled" -> settings.checkForUpdates().set(in.nextBoolean());
|
||||
case "startHidden" -> settings.startHidden().set(in.nextBoolean());
|
||||
case "port" -> settings.port().set(in.nextInt());
|
||||
case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
|
||||
case "preferredGvfsScheme" -> settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
|
||||
case "debugMode" -> settings.debugMode().set(in.nextBoolean());
|
||||
case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
|
||||
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
|
||||
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
|
||||
case "keychainBackend" -> settings.keychainBackend().set(parseKeychainBackend(in.nextString()));
|
||||
case "licenseKey" -> settings.licenseKey().set(in.nextString());
|
||||
case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean());
|
||||
case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean());
|
||||
default -> {
|
||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||
in.skipValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
in.endObject();
|
||||
@@ -132,6 +124,15 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
}
|
||||
}
|
||||
|
||||
private KeychainBackend parseKeychainBackend(String backendName) {
|
||||
try {
|
||||
return KeychainBackend.valueOf(backendName.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Invalid keychain backend {}. Defaulting to {}.", backendName, Settings.DEFAULT_KEYCHAIN_BACKEND);
|
||||
return Settings.DEFAULT_KEYCHAIN_BACKEND;
|
||||
}
|
||||
}
|
||||
|
||||
private NodeOrientation parseUiOrientation(String uiOrientationName) {
|
||||
try {
|
||||
return NodeOrientation.valueOf(uiOrientationName.toUpperCase());
|
||||
@@ -150,5 +151,4 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
in.endArray();
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.LazyInitializer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -48,14 +48,15 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
private static final long SAVE_DELAY_MS = 1000;
|
||||
|
||||
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
|
||||
private final AtomicReference<Settings> settings = new AtomicReference<>();
|
||||
private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter();
|
||||
private final Supplier<Settings> settings = Suppliers.memoize(this::load);
|
||||
private final SettingsJsonAdapter settingsJsonAdapter;
|
||||
private final Environment env;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final Gson gson;
|
||||
|
||||
@Inject
|
||||
public SettingsProvider(Environment env, ScheduledExecutorService scheduler) {
|
||||
public SettingsProvider(SettingsJsonAdapter settingsJsonAdapter, Environment env, ScheduledExecutorService scheduler) {
|
||||
this.settingsJsonAdapter = settingsJsonAdapter;
|
||||
this.env = env;
|
||||
this.scheduler = scheduler;
|
||||
this.gson = new GsonBuilder() //
|
||||
@@ -66,11 +67,11 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
|
||||
@Override
|
||||
public Settings get() {
|
||||
return LazyInitializer.initializeLazily(settings, this::load);
|
||||
return settings.get();
|
||||
}
|
||||
|
||||
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);
|
||||
return settings;
|
||||
}
|
||||
@@ -90,7 +91,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
return Stream.empty();
|
||||
} catch (IOException e) {
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warn("Exception while loading settings from " + path, e);
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
public enum UiTheme {
|
||||
LIGHT("Light"),
|
||||
DARK("Dark");
|
||||
// CUSTOM("Custom (%s)");
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
private String displayName;
|
||||
public enum UiTheme {
|
||||
LIGHT("preferences.general.theme.light"), //
|
||||
DARK("preferences.general.theme.dark"), //
|
||||
AUTOMATIC("preferences.general.theme.automatic");
|
||||
|
||||
public static UiTheme[] applicableValues() {
|
||||
if (SystemUtils.IS_OS_MAC) {
|
||||
return values();
|
||||
} else {
|
||||
return new UiTheme[]{LIGHT, DARK};
|
||||
}
|
||||
}
|
||||
|
||||
private final String displayName;
|
||||
|
||||
UiTheme(String displayName) {
|
||||
this.displayName = displayName;
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -16,17 +20,15 @@ import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The settings specific to a single vault.
|
||||
* TODO: Change the name of individualMountPath and its derivatives to customMountPath
|
||||
*/
|
||||
public class VaultSettings {
|
||||
|
||||
@@ -36,35 +38,32 @@ public class VaultSettings {
|
||||
public static final boolean DEFAULT_USES_READONLY_MODE = false;
|
||||
public static final String DEFAULT_MOUNT_FLAGS = "";
|
||||
public static final int DEFAULT_FILENAME_LENGTH_LIMIT = -1;
|
||||
|
||||
private static final Random RNG = new Random();
|
||||
public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
|
||||
|
||||
private static final Random RNG = new Random();
|
||||
|
||||
private final String id;
|
||||
private final ObjectProperty<Path> path = new SimpleObjectProperty();
|
||||
private final StringProperty mountName = new SimpleStringProperty();
|
||||
private final StringProperty displayName = new SimpleStringProperty();
|
||||
private final StringProperty winDriveLetter = new SimpleStringProperty();
|
||||
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
|
||||
private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REAVEAL_AFTER_MOUNT);
|
||||
private final BooleanProperty usesIndividualMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
|
||||
private final StringProperty individualMountPath = new SimpleStringProperty();
|
||||
private final BooleanProperty useCustomMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
|
||||
private final StringProperty customMountPath = new SimpleStringProperty();
|
||||
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
|
||||
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
|
||||
private final IntegerProperty filenameLengthLimit = new SimpleIntegerProperty(DEFAULT_FILENAME_LENGTH_LIMIT);
|
||||
private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
|
||||
|
||||
private final StringBinding mountName;
|
||||
|
||||
public VaultSettings(String id) {
|
||||
this.id = Objects.requireNonNull(id);
|
||||
|
||||
EasyBind.subscribe(path, this::deriveMountNameFromPath);
|
||||
this.mountName = Bindings.createStringBinding(this::normalizeDisplayName, displayName);
|
||||
}
|
||||
|
||||
Observable[] observables() {
|
||||
return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit};
|
||||
}
|
||||
|
||||
private void deriveMountNameFromPath(Path path) {
|
||||
if (path != null && StringUtils.isBlank(mountName.get())) {
|
||||
mountName.set(normalizeMountName(path.getFileName().toString()));
|
||||
}
|
||||
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit, actionAfterUnlock};
|
||||
}
|
||||
|
||||
public static VaultSettings withRandomId() {
|
||||
@@ -77,23 +76,18 @@ public class VaultSettings {
|
||||
return BaseEncoding.base64Url().encode(randomBytes);
|
||||
}
|
||||
|
||||
public static String normalizeMountName(String mountName) {
|
||||
String normalizedMountName = StringUtils.stripAccents(mountName);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (char c : normalizedMountName.toCharArray()) {
|
||||
if (Character.isWhitespace(c)) {
|
||||
if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
|
||||
builder.append('_');
|
||||
}
|
||||
} else if (c < 127 && Character.isLetterOrDigit(c)) {
|
||||
builder.append(c);
|
||||
} else {
|
||||
if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
|
||||
builder.append('_');
|
||||
}
|
||||
}
|
||||
//visible for testing
|
||||
String normalizeDisplayName() {
|
||||
var original = displayName.getValueSafe();
|
||||
if (original.isBlank() || ".".equals(original) || "..".equals(original)) {
|
||||
return "_";
|
||||
}
|
||||
return builder.toString();
|
||||
|
||||
// replace whitespaces (tabs, linebreaks, ...) by simple space (0x20)
|
||||
var withoutFancyWhitespaces = CharMatcher.whitespace().collapseFrom(original, ' ');
|
||||
|
||||
// replace control chars as well as chars that aren't allowed in file names on standard file systems by underscore
|
||||
return CharMatcher.anyOf("<>:\"/\\|?*").or(CharMatcher.javaIsoControl()).collapseFrom(withoutFancyWhitespaces, '_');
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
@@ -106,7 +100,11 @@ public class VaultSettings {
|
||||
return path;
|
||||
}
|
||||
|
||||
public StringProperty mountName() {
|
||||
public StringProperty displayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public StringBinding mountName() {
|
||||
return mountName;
|
||||
}
|
||||
|
||||
@@ -114,6 +112,14 @@ public class VaultSettings {
|
||||
return winDriveLetter;
|
||||
}
|
||||
|
||||
public Optional<String> getWinDriveLetter() {
|
||||
String current = this.winDriveLetter.get();
|
||||
if (!Strings.isNullOrEmpty(current)) {
|
||||
return Optional.of(current);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public BooleanProperty unlockAfterStartup() {
|
||||
return unlockAfterStartup;
|
||||
}
|
||||
@@ -122,17 +128,17 @@ public class VaultSettings {
|
||||
return revealAfterMount;
|
||||
}
|
||||
|
||||
public BooleanProperty usesIndividualMountPath() {
|
||||
return usesIndividualMountPath;
|
||||
public BooleanProperty useCustomMountPath() {
|
||||
return useCustomMountPath;
|
||||
}
|
||||
|
||||
public StringProperty individualMountPath() {
|
||||
return individualMountPath;
|
||||
public StringProperty customMountPath() {
|
||||
return customMountPath;
|
||||
}
|
||||
|
||||
public Optional<String> getIndividualMountPath() {
|
||||
if (usesIndividualMountPath.get()) {
|
||||
return Optional.ofNullable(Strings.emptyToNull(individualMountPath.get()));
|
||||
public Optional<String> getCustomMountPath() {
|
||||
if (useCustomMountPath.get()) {
|
||||
return Optional.ofNullable(Strings.emptyToNull(customMountPath.get()));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -145,11 +151,19 @@ public class VaultSettings {
|
||||
public StringProperty mountFlags() {
|
||||
return mountFlags;
|
||||
}
|
||||
|
||||
|
||||
public IntegerProperty filenameLengthLimit() {
|
||||
return filenameLengthLimit;
|
||||
}
|
||||
|
||||
public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
|
||||
return actionAfterUnlock;
|
||||
}
|
||||
|
||||
public WhenUnlocked getActionAfterUnlock() {
|
||||
return actionAfterUnlock.get();
|
||||
}
|
||||
|
||||
/* Hashcode/Equals */
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,88 +21,85 @@ class VaultSettingsJsonAdapter {
|
||||
out.beginObject();
|
||||
out.name("id").value(value.getId());
|
||||
out.name("path").value(value.path().get().toString());
|
||||
out.name("mountName").value(value.mountName().get());
|
||||
out.name("displayName").value(value.displayName().get());
|
||||
out.name("winDriveLetter").value(value.winDriveLetter().get());
|
||||
out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
|
||||
out.name("revealAfterMount").value(value.revealAfterMount().get());
|
||||
out.name("usesIndividualMountPath").value(value.usesIndividualMountPath().get());
|
||||
out.name("individualMountPath").value(value.individualMountPath().get());
|
||||
out.name("useCustomMountPath").value(value.useCustomMountPath().get());
|
||||
out.name("customMountPath").value(value.customMountPath().get());
|
||||
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
|
||||
out.name("mountFlags").value(value.mountFlags().get());
|
||||
out.name("filenameLengthLimit").value(value.filenameLengthLimit().get());
|
||||
out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
|
||||
out.endObject();
|
||||
}
|
||||
|
||||
public VaultSettings read(JsonReader in) throws IOException {
|
||||
String id = null;
|
||||
String path = null;
|
||||
String mountName = null;
|
||||
String individualMountPath = null;
|
||||
String mountName = null; //see https://github.com/cryptomator/cryptomator/pull/1318
|
||||
String displayName = null;
|
||||
String customMountPath = null;
|
||||
String winDriveLetter = null;
|
||||
boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
|
||||
boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT;
|
||||
boolean usesIndividualMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
|
||||
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
|
||||
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
|
||||
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
|
||||
int filenameLengthLimit = VaultSettings.DEFAULT_FILENAME_LENGTH_LIMIT;
|
||||
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
|
||||
|
||||
in.beginObject();
|
||||
while (in.hasNext()) {
|
||||
String name = in.nextName();
|
||||
switch (name) {
|
||||
case "id":
|
||||
id = in.nextString();
|
||||
break;
|
||||
case "path":
|
||||
path = in.nextString();
|
||||
break;
|
||||
case "mountName":
|
||||
mountName = in.nextString();
|
||||
break;
|
||||
case "winDriveLetter":
|
||||
winDriveLetter = in.nextString();
|
||||
break;
|
||||
case "unlockAfterStartup":
|
||||
unlockAfterStartup = in.nextBoolean();
|
||||
break;
|
||||
case "revealAfterMount":
|
||||
revealAfterMount = in.nextBoolean();
|
||||
break;
|
||||
case "usesIndividualMountPath":
|
||||
usesIndividualMountPath = in.nextBoolean();
|
||||
break;
|
||||
case "individualMountPath":
|
||||
individualMountPath = in.nextString();
|
||||
break;
|
||||
case "usesReadOnlyMode":
|
||||
usesReadOnlyMode = in.nextBoolean();
|
||||
break;
|
||||
case "mountFlags":
|
||||
mountFlags = in.nextString();
|
||||
break;
|
||||
case "filenameLengthLimit":
|
||||
filenameLengthLimit = in.nextInt();
|
||||
break;
|
||||
default:
|
||||
case "id" -> id = in.nextString();
|
||||
case "path" -> path = in.nextString();
|
||||
case "mountName" -> mountName = in.nextString(); //see https://github.com/cryptomator/cryptomator/pull/1318
|
||||
case "displayName" -> displayName = in.nextString();
|
||||
case "winDriveLetter" -> winDriveLetter = in.nextString();
|
||||
case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean();
|
||||
case "revealAfterMount" -> revealAfterMount = in.nextBoolean();
|
||||
case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
|
||||
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
|
||||
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
|
||||
case "mountFlags" -> mountFlags = in.nextString();
|
||||
case "filenameLengthLimit" -> filenameLengthLimit = in.nextInt();
|
||||
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
|
||||
default -> {
|
||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||
in.skipValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
in.endObject();
|
||||
|
||||
VaultSettings vaultSettings = (id == null) ? VaultSettings.withRandomId() : new VaultSettings(id);
|
||||
vaultSettings.mountName().set(mountName);
|
||||
if (displayName != null) { //see https://github.com/cryptomator/cryptomator/pull/1318
|
||||
vaultSettings.displayName().set(displayName);
|
||||
} else {
|
||||
vaultSettings.displayName().set(mountName);
|
||||
}
|
||||
vaultSettings.path().set(Paths.get(path));
|
||||
vaultSettings.winDriveLetter().set(winDriveLetter);
|
||||
vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
|
||||
vaultSettings.revealAfterMount().set(revealAfterMount);
|
||||
vaultSettings.usesIndividualMountPath().set(usesIndividualMountPath);
|
||||
vaultSettings.individualMountPath().set(individualMountPath);
|
||||
vaultSettings.useCustomMountPath().set(useCustomMountPath);
|
||||
vaultSettings.customMountPath().set(customMountPath);
|
||||
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
|
||||
vaultSettings.mountFlags().set(mountFlags);
|
||||
vaultSettings.filenameLengthLimit().set(filenameLengthLimit);
|
||||
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
|
||||
return vaultSettings;
|
||||
}
|
||||
|
||||
private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) {
|
||||
try {
|
||||
return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Invalid action after unlock {}. Defaulting to {}.", actionAfterUnlockName, VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK);
|
||||
return VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum VolumeImpl {
|
||||
WEBDAV("WebDAV"),
|
||||
FUSE("FUSE"),
|
||||
@@ -17,18 +15,4 @@ public enum VolumeImpl {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a VolumeImpl by display name.
|
||||
*
|
||||
* @param displayName Display name of the VolumeImpl
|
||||
* @return VolumeImpl with the given <code>displayName</code>.
|
||||
* @throws IllegalArgumentException if not volumeImpl with the given <code>displayName</code> was found.
|
||||
*/
|
||||
public static VolumeImpl forDisplayName(String displayName) throws IllegalArgumentException {
|
||||
return Arrays.stream(values()) //
|
||||
.filter(impl -> impl.displayName.equals(displayName)) //
|
||||
.findAny() //
|
||||
.orElseThrow(IllegalArgumentException::new);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum WebDavUrlScheme {
|
||||
DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
|
||||
WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");
|
||||
@@ -9,7 +7,8 @@ public enum WebDavUrlScheme {
|
||||
private final String prefix;
|
||||
private final String displayName;
|
||||
|
||||
WebDavUrlScheme(String prefix, String displayName) {this.prefix = prefix;
|
||||
WebDavUrlScheme(String prefix, String displayName) {
|
||||
this.prefix = prefix;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
@@ -20,18 +19,4 @@ public enum WebDavUrlScheme {
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a WebDavUrlScheme by prefix.
|
||||
*
|
||||
* @param prefix Prefix of the WebDavUrlScheme
|
||||
* @return WebDavUrlScheme with the given <code>prefix</code>.
|
||||
* @throws IllegalArgumentException if not WebDavUrlScheme with the given <code>prefix</code> was found.
|
||||
*/
|
||||
public static WebDavUrlScheme forPrefix(String prefix) throws IllegalArgumentException {
|
||||
return Arrays.stream(values()) //
|
||||
.filter(impl -> impl.prefix.equals(prefix)) //
|
||||
.findAny() //
|
||||
.orElseThrow(IllegalArgumentException::new);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
public enum WhenUnlocked {
|
||||
IGNORE("vaultOptions.general.actionAfterUnlock.ignore"),
|
||||
REVEAL("vaultOptions.general.actionAfterUnlock.reveal"),
|
||||
ASK("vaultOptions.general.actionAfterUnlock.ask");
|
||||
|
||||
private String displayName;
|
||||
|
||||
WhenUnlocked(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class AbstractVolume implements Volume {
|
||||
|
||||
private final Iterable<MountPointChooser> choosers;
|
||||
|
||||
protected Path mountPoint;
|
||||
private boolean cleanupRequired;
|
||||
private MountPointChooser usedChooser;
|
||||
|
||||
public AbstractVolume(Iterable<MountPointChooser> choosers) {
|
||||
this.choosers = choosers;
|
||||
}
|
||||
|
||||
protected Path determineMountPoint() throws InvalidMountPointException {
|
||||
var applicableChoosers = Iterables.filter(choosers, c -> c.isApplicable(this));
|
||||
for (var chooser : applicableChoosers) {
|
||||
Optional<Path> chosenPath = chooser.chooseMountPoint(this);
|
||||
if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint
|
||||
continue;
|
||||
}
|
||||
this.cleanupRequired = chooser.prepare(this, chosenPath.get());
|
||||
this.usedChooser = chooser;
|
||||
return chosenPath.get();
|
||||
}
|
||||
throw new InvalidMountPointException(String.format("No feasible MountPoint found by choosers: %s", applicableChoosers));
|
||||
}
|
||||
|
||||
protected void cleanupMountPoint() {
|
||||
if (this.cleanupRequired) {
|
||||
this.usedChooser.cleanup(this, this.mountPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getMountPoint() {
|
||||
return Optional.ofNullable(mountPoint);
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,5 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface DefaultMountFlags {
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.dokany.Mount;
|
||||
import org.cryptomator.frontend.dokany.MountFactory;
|
||||
@@ -10,33 +12,73 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Named;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
public class DokanyVolume implements Volume {
|
||||
public class DokanyVolume extends AbstractVolume {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class);
|
||||
|
||||
private static final String FS_TYPE_NAME = "Cryptomator File System";
|
||||
private static final String FS_TYPE_NAME = "CryptomatorFS";
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final MountFactory mountFactory;
|
||||
private final WindowsDriveLetters windowsDriveLetters;
|
||||
|
||||
private Mount mount;
|
||||
private Path mountPoint;
|
||||
|
||||
@Inject
|
||||
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, WindowsDriveLetters windowsDriveLetters) {
|
||||
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
||||
super(choosers);
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.mountFactory = new MountFactory(executorService);
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.DOKANY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
try {
|
||||
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip());
|
||||
} catch (MountFailedException e) {
|
||||
if (vaultSettings.getCustomMountPath().isPresent()) {
|
||||
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
|
||||
}
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal(Revealer revealer) throws VolumeException {
|
||||
try {
|
||||
mount.reveal(revealer::reveal);
|
||||
} catch (Exception e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmount() throws VolumeException {
|
||||
try {
|
||||
mount.unmount();
|
||||
} catch (IllegalStateException e) {
|
||||
throw new VolumeException("Unmount Failed.", e);
|
||||
}
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmountForced() {
|
||||
mount.unmountForced();
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsForcedUnmount() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,65 +87,8 @@ public class DokanyVolume implements Volume {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException, IOException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
String mountName = vaultSettings.mountName().get();
|
||||
try {
|
||||
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, mountName, FS_TYPE_NAME, mountFlags.strip());
|
||||
} catch (MountFailedException e) {
|
||||
if (vaultSettings.getIndividualMountPath().isPresent()) {
|
||||
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
|
||||
}
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Path determineMountPoint() throws VolumeException, IOException {
|
||||
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
|
||||
if (optionalCustomMountPoint.isPresent()) {
|
||||
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
|
||||
checkProvidedMountPoint(customMountPoint);
|
||||
return customMountPoint;
|
||||
} else if (!Strings.isNullOrEmpty(vaultSettings.winDriveLetter().get())) {
|
||||
return Path.of(vaultSettings.winDriveLetter().get().charAt(0) + ":\\");
|
||||
} else {
|
||||
//auto assign drive letter
|
||||
if (!windowsDriveLetters.getAvailableDriveLetters().isEmpty()) {
|
||||
return Path.of(windowsDriveLetters.getAvailableDriveLetters().iterator().next() + ":\\");
|
||||
} else {
|
||||
//TODO: Error Handling
|
||||
throw new VolumeException("No free drive letter available.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkProvidedMountPoint(Path mountPoint) throws IOException {
|
||||
if (!Files.isDirectory(mountPoint)) {
|
||||
throw new NotDirectoryException(mountPoint.toString());
|
||||
}
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(mountPoint)) {
|
||||
if (ds.iterator().hasNext()) {
|
||||
throw new DirectoryNotEmptyException(mountPoint.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal() throws VolumeException {
|
||||
boolean success = mount.reveal();
|
||||
if (!success) {
|
||||
throw new VolumeException("Reveal failed.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmount() {
|
||||
mount.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getMountPoint() {
|
||||
return Optional.ofNullable(mountPoint);
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
return MountPointRequirement.EMPTY_MOUNT_POINT;
|
||||
}
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterators;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.fuse.mount.CommandFailedException;
|
||||
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
|
||||
@@ -15,109 +16,66 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import javax.inject.Named;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FuseVolume implements Volume {
|
||||
public class FuseVolume extends AbstractVolume {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class);
|
||||
private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
|
||||
private static final boolean IS_MAC = System.getProperty("os.name").toLowerCase().contains("mac");
|
||||
private static final Pattern NON_WHITESPACE_OR_QUOTED = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); // Thanks to https://stackoverflow.com/a/366532
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Environment environment;
|
||||
|
||||
private Mount fuseMnt;
|
||||
private Path mountPoint;
|
||||
private boolean createdTemporaryMountPoint;
|
||||
private Mount mount;
|
||||
|
||||
@Inject
|
||||
public FuseVolume(VaultSettings vaultSettings, Environment environment) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.environment = environment;
|
||||
public FuseVolume(@Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
||||
super(choosers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws IOException, FuseNotSupportedException, VolumeException {
|
||||
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
|
||||
if (optionalCustomMountPoint.isPresent()) {
|
||||
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
|
||||
checkProvidedMountPoint(customMountPoint);
|
||||
this.mountPoint = customMountPoint;
|
||||
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
|
||||
} else {
|
||||
this.mountPoint = prepareTemporaryMountPoint();
|
||||
LOG.debug("Successfully created mount point: {}", mountPoint);
|
||||
}
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
|
||||
mount(fs.getPath("/"), mountFlags);
|
||||
}
|
||||
|
||||
private void checkProvidedMountPoint(Path mountPoint) throws IOException {
|
||||
if (!Files.isDirectory(mountPoint)) {
|
||||
throw new NotDirectoryException(mountPoint.toString());
|
||||
}
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(mountPoint)) {
|
||||
if (ds.iterator().hasNext()) {
|
||||
throw new DirectoryNotEmptyException(mountPoint.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Path prepareTemporaryMountPoint() throws IOException, VolumeException {
|
||||
Path mountPoint = chooseNonExistingTemporaryMountPoint();
|
||||
// https://github.com/osxfuse/osxfuse/issues/306#issuecomment-245114592:
|
||||
// In order to allow non-admin users to mount FUSE volumes in `/Volumes`,
|
||||
// starting with version 3.5.0, FUSE will create non-existent mount points automatically.
|
||||
if (IS_MAC && mountPoint.getParent().equals(Paths.get("/Volumes"))) {
|
||||
return mountPoint;
|
||||
} else {
|
||||
Files.createDirectories(mountPoint);
|
||||
this.createdTemporaryMountPoint = true;
|
||||
return mountPoint;
|
||||
}
|
||||
}
|
||||
|
||||
private Path chooseNonExistingTemporaryMountPoint() throws VolumeException {
|
||||
Path parent = environment.getMountPointsDir().orElseThrow();
|
||||
String basename = vaultSettings.getId();
|
||||
for (int i = 0; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
|
||||
Path mountPoint = parent.resolve(basename + "_" + i);
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
}
|
||||
LOG.error("Failed to find feasible mountpoint at {}/{}_x. Giving up after {} attempts.", parent, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
|
||||
throw new VolumeException("Did not find feasible mount point.");
|
||||
}
|
||||
|
||||
private void mount(Path root, String mountFlags) throws VolumeException {
|
||||
try {
|
||||
Mounter mounter = FuseMountFactory.getMounter();
|
||||
EnvironmentVariables envVars = EnvironmentVariables.create() //
|
||||
.withFlags(splitFlags(mountFlags)).withMountPoint(mountPoint) //
|
||||
.build();
|
||||
this.fuseMnt = mounter.mount(root, envVars);
|
||||
} catch (CommandFailedException e) {
|
||||
this.mount = mounter.mount(root, envVars);
|
||||
} catch (CommandFailedException | FuseNotSupportedException e) {
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String[] splitFlags(String str) {
|
||||
return Splitter.on(' ').splitToList(str).toArray(String[]::new);
|
||||
List<String> flags = new ArrayList<>();
|
||||
var matches = Iterators.peekingIterator(NON_WHITESPACE_OR_QUOTED.matcher(str).results().iterator());
|
||||
while (matches.hasNext()) {
|
||||
String flag = matches.next().group();
|
||||
// check if flag is missing its argument:
|
||||
if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(1) != null) { // next is "double quoted"
|
||||
// next is "double quoted" and flag is missing its argument
|
||||
flag += matches.next().group(1);
|
||||
} else if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(2) != null) {
|
||||
// next is 'single quoted' and flag is missing its argument
|
||||
flag += matches.next().group(2);
|
||||
}
|
||||
flags.add(flag);
|
||||
}
|
||||
return flags.toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal() throws VolumeException {
|
||||
public void reveal(Revealer revealer) throws VolumeException {
|
||||
try {
|
||||
fuseMnt.revealInFileManager();
|
||||
} catch (CommandFailedException e) {
|
||||
LOG.debug("Revealing the vault in file manger failed: " + e.getMessage());
|
||||
mount.reveal(revealer::reveal);
|
||||
} catch (Exception e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
@@ -130,34 +88,23 @@ public class FuseVolume implements Volume {
|
||||
@Override
|
||||
public synchronized void unmountForced() throws VolumeException {
|
||||
try {
|
||||
fuseMnt.unmountForced();
|
||||
fuseMnt.close();
|
||||
mount.unmountForced();
|
||||
mount.close();
|
||||
} catch (CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupTemporaryMountPoint();
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmount() throws VolumeException {
|
||||
try {
|
||||
fuseMnt.unmount();
|
||||
fuseMnt.close();
|
||||
mount.unmount();
|
||||
mount.close();
|
||||
} catch (CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupTemporaryMountPoint();
|
||||
}
|
||||
|
||||
private void cleanupTemporaryMountPoint() {
|
||||
if (createdTemporaryMountPoint) {
|
||||
try {
|
||||
Files.delete(mountPoint);
|
||||
LOG.debug("Successfully deleted mount point: {}", mountPoint);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Could not delete mount point: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -166,12 +113,17 @@ public class FuseVolume implements Volume {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getMountPoint() {
|
||||
return Optional.ofNullable(mountPoint);
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.FUSE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
return SystemUtils.IS_OS_WINDOWS ? MountPointRequirement.PARENT_NO_MOUNT_POINT : MountPointRequirement.EMPTY_MOUNT_POINT;
|
||||
}
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
return (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_LINUX) && FuseMountFactory.isFuseSupported();
|
||||
return FuseMountFactory.isFuseSupported();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
/**
|
||||
* Enumeration used to indicate the requirements for mounting a vault
|
||||
* using a specific {@link Volume VolumeProvider}, e.g. {@link FuseVolume}.
|
||||
*/
|
||||
public enum MountPointRequirement {
|
||||
|
||||
/**
|
||||
* No Mountpoint on the local filesystem required. (e.g. WebDAV)
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* A parent folder is required, but the actual Mountpoint must not exist.
|
||||
*/
|
||||
PARENT_NO_MOUNT_POINT,
|
||||
|
||||
/**
|
||||
* A parent folder is required, but the actual Mountpoint may exist.
|
||||
*/
|
||||
PARENT_OPT_MOUNT_POINT,
|
||||
|
||||
/**
|
||||
* The actual Mountpoint must exist and must be empty.
|
||||
*/
|
||||
EMPTY_MOUNT_POINT;
|
||||
}
|
||||
@@ -8,6 +8,6 @@ import java.lang.annotation.RetentionPolicy;
|
||||
@Scope
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface PerVault {
|
||||
public @interface PerVault {
|
||||
|
||||
}
|
||||
|
||||
@@ -9,14 +9,10 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.LazyInitializer;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
||||
@@ -31,13 +27,20 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@@ -56,7 +59,7 @@ public class Vault {
|
||||
private final ObjectProperty<VaultState> state;
|
||||
private final ObjectProperty<Exception> lastKnownException;
|
||||
private final VaultStats stats;
|
||||
private final StringBinding displayableName;
|
||||
private final StringBinding displayName;
|
||||
private final StringBinding displayablePath;
|
||||
private final BooleanBinding locked;
|
||||
private final BooleanBinding processing;
|
||||
@@ -66,6 +69,7 @@ public class Vault {
|
||||
private final BooleanBinding unknownError;
|
||||
private final StringBinding accessPoint;
|
||||
private final BooleanBinding accessPointPresent;
|
||||
private final BooleanProperty showingStats;
|
||||
|
||||
private volatile Volume volume;
|
||||
|
||||
@@ -78,7 +82,7 @@ public class Vault {
|
||||
this.state = state;
|
||||
this.lastKnownException = lastKnownException;
|
||||
this.stats = stats;
|
||||
this.displayableName = Bindings.createStringBinding(this::getDisplayableName, vaultSettings.path());
|
||||
this.displayName = Bindings.createStringBinding(this::getDisplayName, vaultSettings.displayName());
|
||||
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
|
||||
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
|
||||
this.processing = Bindings.createBooleanBinding(this::isProcessing, state);
|
||||
@@ -88,22 +92,19 @@ public class Vault {
|
||||
this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
|
||||
this.accessPoint = Bindings.createStringBinding(this::getAccessPoint, state);
|
||||
this.accessPointPresent = this.accessPoint.isNotEmpty();
|
||||
this.showingStats = new SimpleBooleanProperty(false);
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
// Commands
|
||||
// ********************************************************************************/
|
||||
|
||||
private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
|
||||
return LazyInitializer.initializeLazily(cryptoFileSystem, () -> unlockCryptoFileSystem(passphrase), IOException.class);
|
||||
}
|
||||
|
||||
private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
|
||||
private CryptoFileSystem createCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
|
||||
Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
|
||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
||||
flags.add(FileSystemFlags.READONLY);
|
||||
}
|
||||
if (vaultSettings.filenameLengthLimit().get() == -1) {
|
||||
if (!flags.contains(FileSystemFlags.READONLY) && vaultSettings.filenameLengthLimit().get() == -1) {
|
||||
LOG.debug("Determining file name length limitations...");
|
||||
int limit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(getPath());
|
||||
vaultSettings.filenameLengthLimit().set(limit);
|
||||
@@ -120,21 +121,8 @@ public class Vault {
|
||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
||||
}
|
||||
|
||||
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
|
||||
if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
|
||||
throw new NotDirectoryException("");
|
||||
}
|
||||
CryptoFileSystem fs = getCryptoFileSystem(passphrase);
|
||||
volume = volumeProvider.get();
|
||||
volume.mount(fs, getEffectiveMountFlags());
|
||||
}
|
||||
|
||||
public synchronized void lock(boolean forced) throws Volume.VolumeException {
|
||||
if (forced && volume.supportsForcedUnmount()) {
|
||||
volume.unmountForced();
|
||||
} else {
|
||||
volume.unmount();
|
||||
}
|
||||
private void destroyCryptoFileSystem() {
|
||||
LOG.trace("Trying to close associated CryptoFS...");
|
||||
CryptoFileSystem fs = cryptoFileSystem.getAndSet(null);
|
||||
if (fs != null) {
|
||||
try {
|
||||
@@ -145,8 +133,33 @@ public class Vault {
|
||||
}
|
||||
}
|
||||
|
||||
public void reveal() throws Volume.VolumeException {
|
||||
volume.reveal();
|
||||
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
||||
if (cryptoFileSystem.get() == null) {
|
||||
CryptoFileSystem fs = createCryptoFileSystem(passphrase);
|
||||
cryptoFileSystem.set(fs);
|
||||
try {
|
||||
volume = volumeProvider.get();
|
||||
volume.mount(fs, getEffectiveMountFlags());
|
||||
} catch (Exception e) {
|
||||
destroyCryptoFileSystem();
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Already unlocked.");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void lock(boolean forced) throws VolumeException {
|
||||
if (forced && volume.supportsForcedUnmount()) {
|
||||
volume.unmountForced();
|
||||
} else {
|
||||
volume.unmount();
|
||||
}
|
||||
destroyCryptoFileSystem();
|
||||
}
|
||||
|
||||
public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
|
||||
volume.reveal(vaultRevealer);
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
@@ -225,13 +238,12 @@ public class Vault {
|
||||
return state.get() == VaultState.ERROR;
|
||||
}
|
||||
|
||||
public StringBinding displayableNameProperty() {
|
||||
return displayableName;
|
||||
public StringBinding displayNameProperty() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getDisplayableName() {
|
||||
Path p = vaultSettings.path().get();
|
||||
return p.getFileName().toString();
|
||||
public String getDisplayName() {
|
||||
return vaultSettings.displayName().get();
|
||||
}
|
||||
|
||||
public StringBinding accessPointProperty() {
|
||||
@@ -270,6 +282,15 @@ public class Vault {
|
||||
}
|
||||
}
|
||||
|
||||
public BooleanProperty showingStatsProperty() {
|
||||
return showingStats;
|
||||
}
|
||||
|
||||
public boolean isShowingStats() {
|
||||
return accessPointPresent.get();
|
||||
}
|
||||
|
||||
|
||||
// ******************************************************************************
|
||||
// Getter/Setter
|
||||
// *******************************************************************************/
|
||||
@@ -319,6 +340,10 @@ public class Vault {
|
||||
return vaultSettings.getId();
|
||||
}
|
||||
|
||||
public Optional<Volume> getVolume() {
|
||||
return Optional.ofNullable(this.volume);
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
// Hashcode / Equals
|
||||
// *******************************************************************************/
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooserModule;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Named;
|
||||
|
||||
@PerVault
|
||||
@Subcomponent(modules = {VaultModule.class})
|
||||
@Subcomponent(modules = {VaultModule.class, MountPointChooserModule.class})
|
||||
public interface VaultComponent {
|
||||
|
||||
Vault vault();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -20,7 +20,7 @@ class VaultListChangeListener implements ListChangeListener<Vault> {
|
||||
|
||||
@Override
|
||||
public void onChanged(Change<? extends Vault> c) {
|
||||
while(c.next()) {
|
||||
while (c.next()) {
|
||||
if (c.wasAdded()) {
|
||||
List<VaultSettings> addedSettings = c.getAddedSubList().stream().map(Vault::getVaultSettings).collect(Collectors.toList());
|
||||
vaultSettingsList.addAll(c.getFrom(), addedSettings);
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
@@ -19,12 +17,14 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
@@ -36,10 +36,12 @@ public class VaultListManager {
|
||||
|
||||
private final VaultComponent.Builder vaultComponentBuilder;
|
||||
private final ObservableList<Vault> vaultList;
|
||||
private final String defaultVaultName;
|
||||
|
||||
@Inject
|
||||
public VaultListManager(VaultComponent.Builder vaultComponentBuilder, Settings settings) {
|
||||
public VaultListManager(VaultComponent.Builder vaultComponentBuilder, ResourceBundle resourceBundle, Settings settings) {
|
||||
this.vaultComponentBuilder = vaultComponentBuilder;
|
||||
this.defaultVaultName = resourceBundle.getString("defaults.vault.vaultName");
|
||||
this.vaultList = FXCollections.observableArrayList(Vault::observables);
|
||||
|
||||
addAll(settings.getDirectories());
|
||||
@@ -59,14 +61,23 @@ public class VaultListManager {
|
||||
if (alreadyExistingVault.isPresent()) {
|
||||
return alreadyExistingVault.get();
|
||||
} else {
|
||||
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
||||
vaultSettings.path().set(normalizedPathToVault);
|
||||
Vault newVault = create(vaultSettings);
|
||||
Vault newVault = create(newVaultSettings(normalizedPathToVault));
|
||||
vaultList.add(newVault);
|
||||
return newVault;
|
||||
}
|
||||
}
|
||||
|
||||
private VaultSettings newVaultSettings(Path path) {
|
||||
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
||||
vaultSettings.path().set(path);
|
||||
if (path.getFileName() != null) {
|
||||
vaultSettings.displayName().set(path.getFileName().toString());
|
||||
} else {
|
||||
vaultSettings.displayName().set(defaultVaultName);
|
||||
}
|
||||
return vaultSettings;
|
||||
}
|
||||
|
||||
private void addAll(Collection<VaultSettings> vaultSettings) {
|
||||
Collection<Vault> vaults = vaultSettings.stream().map(this::create).collect(Collectors.toList());
|
||||
vaultList.addAll(vaults);
|
||||
@@ -93,7 +104,26 @@ public class VaultListManager {
|
||||
return compBuilder.build().vault();
|
||||
}
|
||||
|
||||
public static VaultState determineVaultState(Path pathToVault) throws IOException {
|
||||
public static VaultState redetermineVaultState(Vault vault) {
|
||||
VaultState previousState = vault.getState();
|
||||
return switch (previousState) {
|
||||
case LOCKED, NEEDS_MIGRATION, MISSING -> {
|
||||
try {
|
||||
VaultState determinedState = determineVaultState(vault.getPath());
|
||||
vault.setState(determinedState);
|
||||
yield determinedState;
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
|
||||
vault.setState(VaultState.ERROR);
|
||||
vault.setLastKnownException(e);
|
||||
yield VaultState.ERROR;
|
||||
}
|
||||
}
|
||||
case ERROR, UNLOCKED, PROCESSING -> previousState;
|
||||
};
|
||||
}
|
||||
|
||||
private static VaultState determineVaultState(Path pathToVault) throws IOException {
|
||||
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
|
||||
return VaultState.MISSING;
|
||||
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
|
||||
|
||||
@@ -7,14 +7,6 @@ package org.cryptomator.common.vaults;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
@@ -25,6 +17,12 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -77,7 +75,7 @@ public class VaultModule {
|
||||
@DefaultMountFlags
|
||||
public StringBinding provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
||||
ObjectProperty<VolumeImpl> preferredVolumeImpl = settings.preferredVolumeImpl();
|
||||
StringProperty mountName = vaultSettings.mountName();
|
||||
StringBinding mountName = vaultSettings.mountName();
|
||||
BooleanProperty readOnly = vaultSettings.usesReadOnlyMode();
|
||||
|
||||
return Bindings.createStringBinding(() -> {
|
||||
@@ -86,6 +84,8 @@ public class VaultModule {
|
||||
return getMacFuseDefaultMountFlags(mountName, readOnly);
|
||||
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_LINUX) {
|
||||
return getLinuxFuseDefaultMountFlags(readOnly);
|
||||
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_WINDOWS) {
|
||||
return getWindowsFuseDefaultMountFlags(mountName, readOnly);
|
||||
} else if (v == VolumeImpl.DOKANY && SystemUtils.IS_OS_WINDOWS) {
|
||||
return getDokanyDefaultMountFlags(readOnly);
|
||||
} else {
|
||||
@@ -95,13 +95,13 @@ public class VaultModule {
|
||||
}
|
||||
|
||||
// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
|
||||
private String getMacFuseDefaultMountFlags(ReadOnlyStringProperty mountName, ReadOnlyBooleanProperty readOnly) {
|
||||
private String getMacFuseDefaultMountFlags(StringBinding mountName, ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_MAC_OSX;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
if (readOnly.get()) {
|
||||
flags.append(" -ordonly");
|
||||
}
|
||||
flags.append(" -ovolname=").append(mountName.get());
|
||||
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
|
||||
flags.append(" -oatomic_o_trunc");
|
||||
flags.append(" -oauto_xattr");
|
||||
flags.append(" -oauto_cache");
|
||||
@@ -144,6 +144,28 @@ public class VaultModule {
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
// see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse_main.c#L53-L62 for syntax guide
|
||||
// see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse.c#L295-L319 for options (-o <...>)
|
||||
// see https://github.com/billziss-gh/winfsp/wiki/Frequently-Asked-Questions/5ba00e4be4f5e938eaae6ef1500b331de12dee77 (FUSE 4.) on why the given defaults were choosen
|
||||
private String getWindowsFuseDefaultMountFlags(StringBinding mountName, ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_WINDOWS;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
|
||||
//WinFSP has no explicit "readonly"-option, nut not setting the group/user-id has the same effect, tho.
|
||||
//So for the time being not setting them is the way to go...
|
||||
//See: https://github.com/billziss-gh/winfsp/issues/319
|
||||
if (!readOnly.get()) {
|
||||
flags.append(" -ouid=-1");
|
||||
flags.append(" -ogid=11");
|
||||
}
|
||||
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
|
||||
//Dokany requires this option to be set, WinFSP doesn't seem to share this peculiarity,
|
||||
//but the option exists. Let's keep this here in case we need it.
|
||||
// flags.append(" -oThreadCount=").append(5);
|
||||
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
// see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
|
||||
private String getDokanyDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_WINDOWS;
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.cryptomator.common.vaults;
|
||||
|
||||
public enum VaultState {
|
||||
/**
|
||||
* No vault found at the provided path
|
||||
* No vault found at the provided path
|
||||
*/
|
||||
MISSING,
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemStats;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -28,6 +30,15 @@ public class VaultStats {
|
||||
private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
|
||||
private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
|
||||
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
|
||||
private final LongProperty bytesPerSecondEncrypted = new SimpleLongProperty();
|
||||
private final LongProperty bytesPerSecondDecrypted = new SimpleLongProperty();
|
||||
private final DoubleProperty cacheHitRate = new SimpleDoubleProperty();
|
||||
private final LongProperty toalBytesRead = new SimpleLongProperty();
|
||||
private final LongProperty toalBytesWritten = new SimpleLongProperty();
|
||||
private final LongProperty totalBytesEncrypted = new SimpleLongProperty();
|
||||
private final LongProperty totalBytesDecrypted = new SimpleLongProperty();
|
||||
private final LongProperty filesRead = new SimpleLongProperty();
|
||||
private final LongProperty filesWritten = new SimpleLongProperty();
|
||||
|
||||
@Inject
|
||||
VaultStats(AtomicReference<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) {
|
||||
@@ -53,12 +64,36 @@ public class VaultStats {
|
||||
|
||||
private void updateStats(Optional<CryptoFileSystemStats> stats) {
|
||||
assert Platform.isFxApplicationThread();
|
||||
bytesPerSecondRead.set(stats.map(CryptoFileSystemStats::pollBytesRead).orElse(0l));
|
||||
bytesPerSecondWritten.set(stats.map(CryptoFileSystemStats::pollBytesWritten).orElse(0l));
|
||||
bytesPerSecondRead.set(stats.map(CryptoFileSystemStats::pollBytesRead).orElse(0L));
|
||||
bytesPerSecondWritten.set(stats.map(CryptoFileSystemStats::pollBytesWritten).orElse(0L));
|
||||
cacheHitRate.set(stats.map(this::getCacheHitRate).orElse(0.0));
|
||||
bytesPerSecondDecrypted.set(stats.map(CryptoFileSystemStats::pollBytesDecrypted).orElse(0L));
|
||||
bytesPerSecondEncrypted.set(stats.map(CryptoFileSystemStats::pollBytesEncrypted).orElse(0L));
|
||||
toalBytesRead.set(stats.map(CryptoFileSystemStats::pollTotalBytesRead).orElse(0L));
|
||||
toalBytesWritten.set(stats.map(CryptoFileSystemStats::pollTotalBytesWritten).orElse(0L));
|
||||
totalBytesEncrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesEncrypted).orElse(0L));
|
||||
totalBytesDecrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesDecrypted).orElse(0L));
|
||||
filesRead.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesRead).orElse(0L));
|
||||
filesWritten.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesWritten).orElse(0L));
|
||||
|
||||
}
|
||||
|
||||
private double getCacheHitRate(CryptoFileSystemStats stats) {
|
||||
long accesses = stats.pollChunkCacheAccesses();
|
||||
long hits = stats.pollChunkCacheHits();
|
||||
if (accesses == 0) {
|
||||
return 0.0;
|
||||
} else {
|
||||
return hits / (double) accesses;
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateStatsService extends ScheduledService<Optional<CryptoFileSystemStats>> {
|
||||
|
||||
private UpdateStatsService() {
|
||||
setOnFailed(event -> LOG.error("Error in UpdateStateService.", getException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Optional<CryptoFileSystemStats>> createTask() {
|
||||
return new Task<>() {
|
||||
@@ -94,4 +129,50 @@ public class VaultStats {
|
||||
public long getBytesPerSecondWritten() {
|
||||
return bytesPerSecondWritten.get();
|
||||
}
|
||||
|
||||
public LongProperty bytesPerSecondEncryptedProperty() {
|
||||
return bytesPerSecondEncrypted;
|
||||
}
|
||||
|
||||
public long getBytesPerSecondEnrypted() {
|
||||
return bytesPerSecondEncrypted.get();
|
||||
}
|
||||
|
||||
public LongProperty bytesPerSecondDecryptedProperty() {
|
||||
return bytesPerSecondDecrypted;
|
||||
}
|
||||
|
||||
public long getBytesPerSecondDecrypted() {
|
||||
return bytesPerSecondDecrypted.get();
|
||||
}
|
||||
|
||||
public DoubleProperty cacheHitRateProperty() { return cacheHitRate; }
|
||||
|
||||
public double getCacheHitRate() {
|
||||
return cacheHitRate.get();
|
||||
}
|
||||
|
||||
public LongProperty toalBytesReadProperty() {return toalBytesRead;}
|
||||
|
||||
public long getTotalBytesRead() { return toalBytesRead.get();}
|
||||
|
||||
public LongProperty toalBytesWrittenProperty() {return toalBytesWritten;}
|
||||
|
||||
public long getTotalBytesWritten() { return toalBytesWritten.get();}
|
||||
|
||||
public LongProperty totalBytesEncryptedProperty() {return totalBytesEncrypted;}
|
||||
|
||||
public long getTotalBytesEncrypted() { return totalBytesEncrypted.get();}
|
||||
|
||||
public LongProperty totalBytesDecryptedProperty() {return totalBytesDecrypted;}
|
||||
|
||||
public long getTotalBytesDecrypted() { return totalBytesDecrypted.get();}
|
||||
|
||||
public LongProperty filesRead() { return filesRead;}
|
||||
|
||||
public long getFilesRead() { return filesRead.get();}
|
||||
|
||||
public LongProperty filesWritten() {return filesWritten;}
|
||||
|
||||
public long getFilesWritten() {return filesWritten.get();}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
|
||||
@@ -20,18 +21,35 @@ public interface Volume {
|
||||
*/
|
||||
boolean isSupported();
|
||||
|
||||
/**
|
||||
* Gets the coresponding enum type of the {@link VolumeImpl volume implementation ("VolumeImpl")} that is implemented by this Volume.
|
||||
*
|
||||
* @return the type of implementation as defined by the {@link VolumeImpl VolumeImpl enum}
|
||||
*/
|
||||
VolumeImpl getImplementationType();
|
||||
|
||||
/**
|
||||
* @param fs
|
||||
* @throws IOException
|
||||
*/
|
||||
void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException;
|
||||
void mount(CryptoFileSystem fs, String mountFlags) 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;
|
||||
|
||||
Optional<Path> getMountPoint();
|
||||
|
||||
MountPointRequirement getMountPointRequirement();
|
||||
|
||||
// optional forced unmounting:
|
||||
|
||||
default boolean supportsForcedUnmount() {
|
||||
@@ -69,4 +87,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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||
import org.cryptomator.frontend.webdav.mount.MountParams;
|
||||
@@ -15,6 +17,7 @@ import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class WebDavVolume implements Volume {
|
||||
|
||||
@@ -23,55 +26,69 @@ public class WebDavVolume implements Volume {
|
||||
private final Provider<WebDavServer> serverProvider;
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Settings settings;
|
||||
private final WindowsDriveLetters windowsDriveLetters;
|
||||
|
||||
private WebDavServer server;
|
||||
private WebDavServletController servlet;
|
||||
private Mounter.Mount mount;
|
||||
private Path mountPoint;
|
||||
|
||||
@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.vaultSettings = vaultSettings;
|
||||
this.settings = settings;
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException {
|
||||
startServlet(fs);
|
||||
mountServlet();
|
||||
}
|
||||
|
||||
private void startServlet(CryptoFileSystem fs){
|
||||
if (server == null) {
|
||||
server = serverProvider.get();
|
||||
}
|
||||
if (!server.isRunning()) {
|
||||
server.start();
|
||||
}
|
||||
servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + vaultSettings.mountName().get());
|
||||
CharMatcher acceptable = CharMatcher.inRange('0', '9').or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.inRange('a', 'z'));
|
||||
String urlConformMountName = acceptable.negate().collapseFrom(vaultSettings.mountName().get(), '_');
|
||||
servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + urlConformMountName);
|
||||
servlet.start();
|
||||
mount();
|
||||
}
|
||||
|
||||
private void mount() throws VolumeException {
|
||||
private void mountServlet() throws VolumeException {
|
||||
if (servlet == null) {
|
||||
throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
|
||||
}
|
||||
|
||||
//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a 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() //
|
||||
.withWindowsDriveLetter(vaultSettings.winDriveLetter().get()) //
|
||||
.withWindowsDriveLetter(driveLetterSupplier.get()) //
|
||||
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
|
||||
.withWebdavHostname(getLocalhostAliasOrNull()) //
|
||||
.build();
|
||||
try {
|
||||
this.mount = servlet.mount(mountParams); // might block this thread for a while
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
e.printStackTrace();
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal() throws VolumeException {
|
||||
public void reveal(Revealer revealer) throws VolumeException {
|
||||
try {
|
||||
mount.reveal();
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
e.printStackTrace();
|
||||
mount.reveal(revealer::reveal);
|
||||
} catch (Exception e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
@@ -98,7 +115,12 @@ public class WebDavVolume implements Volume {
|
||||
|
||||
@Override
|
||||
public Optional<Path> getMountPoint() {
|
||||
return Optional.ofNullable(mountPoint);
|
||||
return mount.getMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
return MountPointRequirement.NONE;
|
||||
}
|
||||
|
||||
private String getLocalhostAliasOrNull() {
|
||||
@@ -126,6 +148,11 @@ public class WebDavVolume implements Volume {
|
||||
return WebDavVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.WEBDAV;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsForcedUnmount() {
|
||||
return mount != null && mount.forced().isPresent();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
@@ -12,6 +13,7 @@ import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -24,7 +26,7 @@ public final class WindowsDriveLetters {
|
||||
|
||||
static {
|
||||
try (IntStream stream = IntStream.rangeClosed('C', 'Z')) {
|
||||
C_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(Collectors.toSet());
|
||||
C_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(ImmutableSet.toImmutableSet());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +48,18 @@ public final class WindowsDriveLetters {
|
||||
}
|
||||
|
||||
public Set<String> getAvailableDriveLetters() {
|
||||
return Sets.difference(C_TO_Z, getOccupiedDriveLetters());
|
||||
return Sets.difference(getAllDriveLetters(), getOccupiedDriveLetters());
|
||||
}
|
||||
|
||||
public Optional<String> getAvailableDriveLetter() {
|
||||
return getAvailableDriveLetters().stream().findFirst();
|
||||
}
|
||||
|
||||
public Optional<Path> getAvailableDriveLetterPath() {
|
||||
return getAvailableDriveLetter().map(this::toPath);
|
||||
}
|
||||
|
||||
public Path toPath(String driveLetter) {
|
||||
return Path.of(driveLetter + ":\\");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -20,14 +21,10 @@ class EnvironmentTest {
|
||||
|
||||
private Environment env;
|
||||
|
||||
@BeforeAll
|
||||
static void init() {
|
||||
System.setProperty("user.home", "/home/testuser");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void initEach() {
|
||||
env = new Environment();
|
||||
void init() {
|
||||
env = Mockito.spy(new Environment());
|
||||
Mockito.when(env.getHomeDir()).thenReturn(Path.of("/home/testuser"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -37,7 +34,7 @@ class EnvironmentTest {
|
||||
|
||||
List<Path> result = env.getSettingsPath().collect(Collectors.toList());
|
||||
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")));
|
||||
}
|
||||
|
||||
@@ -48,7 +45,7 @@ class EnvironmentTest {
|
||||
|
||||
List<Path> result = env.getIpcPortPath().collect(Collectors.toList());
|
||||
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")));
|
||||
}
|
||||
|
||||
@@ -123,8 +120,8 @@ class EnvironmentTest {
|
||||
List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList());
|
||||
|
||||
MatcherAssert.assertThat(result, Matchers.hasSize(3));
|
||||
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/test"),
|
||||
Paths.get("/home/testuser/test2"),
|
||||
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/test"), //
|
||||
Paths.get("/home/testuser/test2"), //
|
||||
Paths.get("/foo/bar/test")));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import org.cryptomator.common.LicenseChecker;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -14,7 +13,7 @@ class LicenseCheckerTest {
|
||||
+ "PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47" //
|
||||
+ "6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM" //
|
||||
+ "Al8G7CqwoJOsW7Kddns=";
|
||||
|
||||
|
||||
private LicenseChecker licenseChecker;
|
||||
|
||||
@BeforeEach
|
||||
@@ -25,9 +24,9 @@ class LicenseCheckerTest {
|
||||
@Test
|
||||
public void testCheckValidLicense() {
|
||||
String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
|
||||
|
||||
|
||||
Optional<DecodedJWT> decoded = licenseChecker.check(license);
|
||||
|
||||
|
||||
Assertions.assertTrue(decoded.isPresent());
|
||||
Assertions.assertEquals("cryptobot@example.com", decoded.get().getSubject());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.cryptomator.common.keychain;
|
||||
|
||||
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
|
||||
public class KeychainManagerTest {
|
||||
|
||||
@Test
|
||||
public void testStoreAndLoad() throws KeychainAccessException {
|
||||
KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess()));
|
||||
keychainManager.storePassphrase("test", "asd");
|
||||
Assertions.assertArrayEquals("asd".toCharArray(), keychainManager.loadPassphrase("test"));
|
||||
}
|
||||
|
||||
@Nested
|
||||
public static class WhenObservingProperties {
|
||||
|
||||
@BeforeAll
|
||||
public static void startup() throws InterruptedException {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Platform.startup(latch::countDown);
|
||||
latch.await(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPropertyChangesWhenStoringPassword() throws KeychainAccessException, InterruptedException {
|
||||
KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess()));
|
||||
ReadOnlyBooleanProperty property = keychainManager.getPassphraseStoredProperty("test");
|
||||
Assertions.assertEquals(false, property.get());
|
||||
|
||||
keychainManager.storePassphrase("test", "bar");
|
||||
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Platform.runLater(() -> {
|
||||
result.set(property.get());
|
||||
latch.countDown();
|
||||
});
|
||||
latch.await(1, TimeUnit.SECONDS);
|
||||
Assertions.assertEquals(true, result.get());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,12 +3,14 @@
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.keychain;
|
||||
package org.cryptomator.common.keychain;
|
||||
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class MapKeychainAccess implements KeychainAccessStrategy {
|
||||
class MapKeychainAccess implements KeychainAccessProvider {
|
||||
|
||||
private final Map<String, char[]> map = new HashMap<>();
|
||||
|
||||
@@ -31,6 +33,12 @@ class MapKeychainAccess implements KeychainAccessStrategy {
|
||||
map.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassphrase(String key, CharSequence passphrase) {
|
||||
map.get(key);
|
||||
storePassphrase(key, passphrase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return true;
|
||||
@@ -5,16 +5,19 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
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
|
||||
public void testDeserialize() throws IOException {
|
||||
@@ -37,10 +40,10 @@ public class SettingsJsonAdapterTest {
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "fromJson() should throw IOException for input: {0}")
|
||||
@ValueSource(strings = {
|
||||
"",
|
||||
"<html>",
|
||||
"{invalidjson}"
|
||||
@ValueSource(strings = { //
|
||||
"", //
|
||||
"<html>", //
|
||||
"{invalidjson}" //
|
||||
})
|
||||
public void testDeserializeMalformed(String input) {
|
||||
Assertions.assertThrows(IOException.class, () -> {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
@@ -14,8 +15,10 @@ public class SettingsTest {
|
||||
|
||||
@Test
|
||||
public void testAutoSave() {
|
||||
Environment env = Mockito.mock(Environment.class);
|
||||
@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
|
||||
Settings settings = new Settings();
|
||||
|
||||
Settings settings = new Settings(env);
|
||||
settings.setSaveCmd(changeListener);
|
||||
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
||||
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
|
||||
@@ -29,7 +32,7 @@ public class SettingsTest {
|
||||
Mockito.verify(changeListener, Mockito.times(2)).accept(settings);
|
||||
|
||||
// third change (to property of list item):
|
||||
vaultSettings.mountName().set("asd");
|
||||
vaultSettings.displayName().set("asd");
|
||||
Mockito.verify(changeListener, Mockito.times(3)).accept(settings);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class VaultSettingsJsonAdapterTest {
|
||||
|
||||
@@ -24,15 +23,15 @@ public class VaultSettingsJsonAdapterTest {
|
||||
|
||||
@Test
|
||||
public void testDeserialize() throws IOException {
|
||||
String json = "{\"id\": \"foo\", \"path\": \"/foo/bar\", \"mountName\": \"test\", \"winDriveLetter\": \"X\", \"shouldBeIgnored\": true, \"individualMountPath\": \"/home/test/crypto\", \"mountFlags\":\"--foo --bar\"}";
|
||||
String json = "{\"id\": \"foo\", \"path\": \"/foo/bar\", \"displayName\": \"test\", \"winDriveLetter\": \"X\", \"shouldBeIgnored\": true, \"individualMountPath\": \"/home/test/crypto\", \"mountFlags\":\"--foo --bar\"}";
|
||||
JsonReader jsonReader = new JsonReader(new StringReader(json));
|
||||
|
||||
VaultSettings vaultSettings = adapter.read(jsonReader);
|
||||
Assertions.assertEquals("foo", vaultSettings.getId());
|
||||
Assertions.assertEquals(Paths.get("/foo/bar"), vaultSettings.path().get());
|
||||
Assertions.assertEquals("test", vaultSettings.mountName().get());
|
||||
Assertions.assertEquals("test", vaultSettings.displayName().get());
|
||||
Assertions.assertEquals("X", vaultSettings.winDriveLetter().get());
|
||||
Assertions.assertEquals("/home/test/crypto", vaultSettings.individualMountPath().get());
|
||||
Assertions.assertEquals("/home/test/crypto", vaultSettings.customMountPath().get());
|
||||
Assertions.assertEquals("--foo --bar", vaultSettings.mountFlags().get());
|
||||
|
||||
|
||||
@@ -42,7 +41,7 @@ public class VaultSettingsJsonAdapterTest {
|
||||
public void testSerialize() throws IOException {
|
||||
VaultSettings vaultSettings = new VaultSettings("test");
|
||||
vaultSettings.path().set(Paths.get("/foo/bar"));
|
||||
vaultSettings.mountName().set("mountyMcMountFace");
|
||||
vaultSettings.displayName().set("mountyMcMountFace");
|
||||
vaultSettings.mountFlags().set("--foo --bar");
|
||||
|
||||
StringWriter buf = new StringWriter();
|
||||
@@ -51,12 +50,12 @@ public class VaultSettingsJsonAdapterTest {
|
||||
String result = buf.toString();
|
||||
|
||||
MatcherAssert.assertThat(result, CoreMatchers.containsString("\"id\":\"test\""));
|
||||
if(System.getProperty("os.name").contains("Windows")){
|
||||
if (System.getProperty("os.name").contains("Windows")) {
|
||||
MatcherAssert.assertThat(result, CoreMatchers.containsString("\"path\":\"\\\\foo\\\\bar\""));
|
||||
} else {
|
||||
MatcherAssert.assertThat(result, CoreMatchers.containsString("\"path\":\"/foo/bar\""));
|
||||
}
|
||||
MatcherAssert.assertThat(result, CoreMatchers.containsString("\"mountName\":\"mountyMcMountFace\""));
|
||||
MatcherAssert.assertThat(result, CoreMatchers.containsString("\"displayName\":\"mountyMcMountFace\""));
|
||||
MatcherAssert.assertThat(result, CoreMatchers.containsString("\"mountFlags\":\"--foo --bar\""));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class VaultSettingsTest {
|
||||
|
||||
@Test
|
||||
public void testNormalize() throws Exception {
|
||||
assertEquals("_", VaultSettings.normalizeMountName(" "));
|
||||
assertEquals("a", VaultSettings.normalizeMountName("ä"));
|
||||
assertEquals("C", VaultSettings.normalizeMountName("Ĉ"));
|
||||
assertEquals("_", VaultSettings.normalizeMountName(":"));
|
||||
assertEquals("_", VaultSettings.normalizeMountName("汉语"));
|
||||
@ParameterizedTest
|
||||
@CsvSource({"a\u000Fa,a_a", ": \\,_ _", "汉语,汉语", "..,_", "a\ta,a\u0020a", "\t\n\r,_"})
|
||||
public void testNormalize(String test, String expected) {
|
||||
VaultSettings settings = new VaultSettings("id");
|
||||
settings.displayName().setValue(test);
|
||||
assertEquals(expected, settings.normalizeDisplayName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
@@ -17,6 +13,11 @@ import org.junit.jupiter.api.condition.OS;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class VaultModuleTest {
|
||||
@@ -28,8 +29,9 @@ public class VaultModuleTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setup(@TempDir Path tmpDir) {
|
||||
Mockito.when(vaultSettings.mountName()).thenReturn(new SimpleStringProperty("TEST"));
|
||||
Mockito.when(vaultSettings.mountName()).thenReturn(Bindings.createStringBinding(() -> "TEST"));
|
||||
Mockito.when(vaultSettings.usesReadOnlyMode()).thenReturn(new SimpleBooleanProperty(true));
|
||||
Mockito.when(vaultSettings.displayName()).thenReturn(new SimpleStringProperty("Vault"));
|
||||
System.setProperty("user.home", tmpDir.toString());
|
||||
}
|
||||
|
||||
@@ -41,7 +43,7 @@ public class VaultModuleTest {
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.5.2</version>
|
||||
</parent>
|
||||
<artifactId>keychain</artifactId>
|
||||
<name>System Keychain Access</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Google -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DI -->
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- secret-service lib -->
|
||||
<dependency>
|
||||
<groupId>de.swiesend</groupId>
|
||||
<artifactId>secret-service</artifactId>
|
||||
<version>1.0.0-RC.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,31 +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.keychain;
|
||||
|
||||
public interface KeychainAccess {
|
||||
|
||||
/**
|
||||
* Associates a passphrase with a given key.
|
||||
*
|
||||
* @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}.
|
||||
* @param passphrase The secret to store in this keychain.
|
||||
*/
|
||||
void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException;
|
||||
|
||||
/**
|
||||
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
|
||||
* @return The stored passphrase for the given key or <code>null</code> if no value for the given key could be found.
|
||||
*/
|
||||
char[] loadPassphrase(String key) throws KeychainAccessException;
|
||||
|
||||
/**
|
||||
* Deletes a passphrase with a given key.
|
||||
*
|
||||
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
|
||||
*/
|
||||
void deletePassphrase(String key) throws KeychainAccessException;
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
/**
|
||||
* Indicates an error during communication with the operating system's keychain.
|
||||
*/
|
||||
public class KeychainAccessException extends Exception {
|
||||
|
||||
KeychainAccessException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +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.keychain;
|
||||
|
||||
interface KeychainAccessStrategy extends KeychainAccess {
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if this KeychainAccessStrategy works on the current machine.
|
||||
* @implNote This method must not throw any exceptions and should fail fast
|
||||
* returning <code>false</code> if it can't determine availability of the checked strategy
|
||||
*/
|
||||
boolean isSupported();
|
||||
|
||||
}
|
||||
@@ -1,33 +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.keychain;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.ElementsIntoSet;
|
||||
import org.cryptomator.common.JniModule;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@Module(includes = {JniModule.class})
|
||||
public class KeychainModule {
|
||||
|
||||
@Provides
|
||||
@ElementsIntoSet
|
||||
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) {
|
||||
return Sets.newHashSet(macKeychain, winKeychain, linKeychain);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public Optional<KeychainAccess> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
|
||||
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A facade to LinuxSecretServiceKeychainAccessImpl that doesn't depend on libraries that are unavailable on Mac and Windows.
|
||||
*/
|
||||
public class LinuxSecretServiceKeychainAccess implements KeychainAccessStrategy {
|
||||
|
||||
// the actual implementation is hidden in this delegate object which is loaded via reflection,
|
||||
// as it depends on libraries that aren't necessarily available:
|
||||
private final Optional<KeychainAccessStrategy> delegate;
|
||||
|
||||
@Inject
|
||||
public LinuxSecretServiceKeychainAccess() {
|
||||
this.delegate = constructGnomeKeyringKeychainAccess();
|
||||
}
|
||||
|
||||
private static Optional<KeychainAccessStrategy> constructGnomeKeyringKeychainAccess() {
|
||||
try {
|
||||
Class<?> clazz = Class.forName("org.cryptomator.keychain.LinuxSecretServiceKeychainAccessImpl");
|
||||
KeychainAccessStrategy instance = (KeychainAccessStrategy) clazz.getDeclaredConstructor().newInstance();
|
||||
return Optional.of(instance);
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return SystemUtils.IS_OS_LINUX && delegate.map(KeychainAccessStrategy::isSupported).orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
|
||||
delegate.orElseThrow(IllegalStateException::new).storePassphrase(key, passphrase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] loadPassphrase(String key) throws KeychainAccessException {
|
||||
return delegate.orElseThrow(IllegalStateException::new).loadPassphrase(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePassphrase(String key) throws KeychainAccessException {
|
||||
delegate.orElseThrow(IllegalStateException::new).deletePassphrase(key);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import org.freedesktop.secret.simple.SimpleCollection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
try (@SuppressWarnings("unused") SimpleCollection keyring = new SimpleCollection()) {
|
||||
// seems like we're able to access the keyring.
|
||||
return true;
|
||||
} catch (IOException | RuntimeException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
|
||||
try (SimpleCollection keyring = new SimpleCollection()) {
|
||||
List<String> list = keyring.getItems(createAttributes(key));
|
||||
if (list == null) {
|
||||
keyring.createItem("Cryptomator", passphrase, createAttributes(key));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new KeychainAccessException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] loadPassphrase(String key) throws KeychainAccessException {
|
||||
try (SimpleCollection keyring = new SimpleCollection()) {
|
||||
List<String> list = keyring.getItems(createAttributes(key));
|
||||
if (list != null) {
|
||||
return keyring.getSecret(list.get(0));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new KeychainAccessException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePassphrase(String key) throws KeychainAccessException {
|
||||
try (SimpleCollection keyring = new SimpleCollection()) {
|
||||
List<String> list = keyring.getItems(createAttributes(key));
|
||||
if (list != null) {
|
||||
keyring.deleteItem(list.get(0));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new KeychainAccessException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> createAttributes(String key) {
|
||||
Map<String, String> attributes = new HashMap();
|
||||
attributes.put("Vault", key);
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +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.keychain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.jni.MacFunctions;
|
||||
import org.cryptomator.jni.MacKeychainAccess;
|
||||
|
||||
class MacSystemKeychainAccess implements KeychainAccessStrategy {
|
||||
|
||||
private final Optional<MacFunctions> macFunctions;
|
||||
|
||||
@Inject
|
||||
public MacSystemKeychainAccess(Optional<MacFunctions> macFunctions) {
|
||||
this.macFunctions = macFunctions;
|
||||
}
|
||||
|
||||
private MacKeychainAccess keychain() {
|
||||
return macFunctions.orElseThrow(IllegalStateException::new).keychainAccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePassphrase(String key, CharSequence passphrase) {
|
||||
keychain().storePassword(key, passphrase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] loadPassphrase(String key) {
|
||||
return keychain().loadPassword(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return SystemUtils.IS_OS_MAC_OSX && macFunctions.isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePassphrase(String key) {
|
||||
keychain().deletePassword(key);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,199 +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.keychain;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.jni.WinDataProtection;
|
||||
import org.cryptomator.jni.WinFunctions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Reader;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.io.Writer;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class);
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting() //
|
||||
.registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) //
|
||||
.disableHtmlEscaping().create();
|
||||
|
||||
private final Optional<WinFunctions> winFunctions;
|
||||
private final List<Path> keychainPaths;
|
||||
private Map<String, KeychainEntry> keychainEntries;
|
||||
|
||||
@Inject
|
||||
public WindowsProtectedKeychainAccess(Optional<WinFunctions> winFunctions, Environment environment) {
|
||||
this.winFunctions = winFunctions;
|
||||
this.keychainPaths = environment.getKeychainPath().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private WinDataProtection dataProtection() {
|
||||
return winFunctions.orElseThrow(IllegalStateException::new).dataProtection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePassphrase(String key, CharSequence passphrase) {
|
||||
loadKeychainEntriesIfNeeded();
|
||||
ByteBuffer buf = UTF_8.encode(CharBuffer.wrap(passphrase));
|
||||
byte[] cleartext = new byte[buf.remaining()];
|
||||
buf.get(cleartext);
|
||||
KeychainEntry entry = new KeychainEntry();
|
||||
entry.salt = generateSalt();
|
||||
entry.ciphertext = dataProtection().protect(cleartext, entry.salt);
|
||||
Arrays.fill(buf.array(), (byte) 0x00);
|
||||
Arrays.fill(cleartext, (byte) 0x00);
|
||||
keychainEntries.put(key, entry);
|
||||
saveKeychainEntries();
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] loadPassphrase(String key) {
|
||||
loadKeychainEntriesIfNeeded();
|
||||
KeychainEntry entry = keychainEntries.get(key);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] cleartext = dataProtection().unprotect(entry.ciphertext, entry.salt);
|
||||
if (cleartext == null) {
|
||||
return null;
|
||||
}
|
||||
CharBuffer buf = UTF_8.decode(ByteBuffer.wrap(cleartext));
|
||||
char[] passphrase = new char[buf.remaining()];
|
||||
buf.get(passphrase);
|
||||
Arrays.fill(cleartext, (byte) 0x00);
|
||||
Arrays.fill(buf.array(), (char) 0x00);
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePassphrase(String key) {
|
||||
loadKeychainEntriesIfNeeded();
|
||||
keychainEntries.remove(key);
|
||||
saveKeychainEntries();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return SystemUtils.IS_OS_WINDOWS && winFunctions.isPresent() && !keychainPaths.isEmpty();
|
||||
}
|
||||
|
||||
private byte[] generateSalt() {
|
||||
byte[] result = new byte[2 * Long.BYTES];
|
||||
UUID uuid = UUID.randomUUID();
|
||||
ByteBuffer buf = ByteBuffer.wrap(result);
|
||||
buf.putLong(uuid.getMostSignificantBits());
|
||||
buf.putLong(uuid.getLeastSignificantBits());
|
||||
return result;
|
||||
}
|
||||
|
||||
private void loadKeychainEntriesIfNeeded() {
|
||||
if (keychainEntries == null) {
|
||||
for (Path keychainPath : keychainPaths) {
|
||||
Optional<Map<String, KeychainEntry>> keychain = loadKeychainEntries(keychainPath);
|
||||
if (keychain.isPresent()) {
|
||||
keychainEntries = keychain.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (keychainEntries == null) {
|
||||
LOG.info("Unable to load existing keychain file, creating new keychain.");
|
||||
keychainEntries = new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Map<String, KeychainEntry>> loadKeychainEntries(Path keychainPath) {
|
||||
LOG.debug("Attempting to load keychain from {}", keychainPath);
|
||||
Type type = new TypeToken<Map<String, KeychainEntry>>() {
|
||||
}.getType();
|
||||
try (InputStream in = Files.newInputStream(keychainPath, StandardOpenOption.READ); //
|
||||
Reader reader = new InputStreamReader(in, UTF_8)) {
|
||||
return Optional.of(GSON.fromJson(reader, type));
|
||||
} catch (NoSuchFileException | JsonParseException e) {
|
||||
return Optional.empty();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveKeychainEntries() {
|
||||
if (keychainPaths.isEmpty()) {
|
||||
throw new IllegalStateException("Can't save keychain if no keychain path is specified.");
|
||||
}
|
||||
saveKeychainEntries(keychainPaths.get(0));
|
||||
}
|
||||
|
||||
private void saveKeychainEntries(Path keychainPath) {
|
||||
try (OutputStream out = Files.newOutputStream(keychainPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); //
|
||||
Writer writer = new OutputStreamWriter(out, UTF_8)) {
|
||||
GSON.toJson(keychainEntries, writer);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class KeychainEntry {
|
||||
|
||||
@SerializedName("ciphertext")
|
||||
byte[] ciphertext;
|
||||
@SerializedName("salt")
|
||||
byte[] salt;
|
||||
}
|
||||
|
||||
private static class ByteArrayJsonAdapter implements JsonSerializer<byte[]>, JsonDeserializer<byte[]> {
|
||||
|
||||
private static final BaseEncoding BASE64 = BaseEncoding.base64();
|
||||
|
||||
@Override
|
||||
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return BASE64.decode(json.getAsString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(BASE64.encode(src));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +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.keychain;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class KeychainModuleTest {
|
||||
|
||||
@Test
|
||||
public void testGetKeychain() throws KeychainAccessException {
|
||||
Optional<KeychainAccess> keychainAccess = DaggerTestKeychainComponent.builder().keychainModule(new TestKeychainModule()).build().keychainAccess();
|
||||
Assertions.assertTrue(keychainAccess.isPresent());
|
||||
Assertions.assertTrue(keychainAccess.get() instanceof MapKeychainAccess);
|
||||
keychainAccess.get().storePassphrase("test", "asd");
|
||||
Assertions.assertArrayEquals("asd".toCharArray(), keychainAccess.get().loadPassphrase("test"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +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.keychain;
|
||||
|
||||
import dagger.Component;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = KeychainModule.class)
|
||||
interface TestKeychainComponent {
|
||||
|
||||
Optional<KeychainAccess> keychainAccess();
|
||||
|
||||
}
|
||||
@@ -1,17 +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.keychain;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class TestKeychainModule extends KeychainModule {
|
||||
|
||||
@Override
|
||||
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) {
|
||||
return Set.of(new MapKeychainAccess());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,56 +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.keychain;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.jni.WinDataProtection;
|
||||
import org.cryptomator.jni.WinFunctions;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class WindowsProtectedKeychainAccessTest {
|
||||
|
||||
private WindowsProtectedKeychainAccess keychain;
|
||||
|
||||
@BeforeEach
|
||||
public void setup(@TempDir Path tempDir) {
|
||||
Path keychainPath = tempDir.resolve("keychainfile.tmp");
|
||||
Environment env = Mockito.mock(Environment.class);
|
||||
Mockito.when(env.getKeychainPath()).thenReturn(Stream.of(keychainPath));
|
||||
WinFunctions winFunctions = Mockito.mock(WinFunctions.class);
|
||||
WinDataProtection winDataProtection = Mockito.mock(WinDataProtection.class);
|
||||
Mockito.when(winFunctions.dataProtection()).thenReturn(winDataProtection);
|
||||
Answer<byte[]> answerReturningFirstArg = invocation -> ((byte[]) invocation.getArgument(0)).clone();
|
||||
Mockito.when(winDataProtection.protect(Mockito.any(), Mockito.any())).thenAnswer(answerReturningFirstArg);
|
||||
Mockito.when(winDataProtection.unprotect(Mockito.any(), Mockito.any())).thenAnswer(answerReturningFirstArg);
|
||||
keychain = new WindowsProtectedKeychainAccess(Optional.of(winFunctions), env);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStoreAndLoad() {
|
||||
String storedPw1 = "topSecret";
|
||||
String storedPw2 = "bottomSecret";
|
||||
keychain.storePassphrase("myPassword", storedPw1);
|
||||
keychain.storePassphrase("myOtherPassword", storedPw2);
|
||||
String loadedPw1 = new String(keychain.loadPassphrase("myPassword"));
|
||||
String loadedPw2 = new String(keychain.loadPassphrase("myOtherPassword"));
|
||||
Assertions.assertEquals(storedPw1, loadedPw1);
|
||||
Assertions.assertEquals(storedPw2, loadedPw2);
|
||||
keychain.deletePassphrase("myPassword");
|
||||
Assertions.assertNull(keychain.loadPassphrase("myPassword"));
|
||||
Assertions.assertNotNull(keychain.loadPassphrase("myOtherPassword"));
|
||||
Assertions.assertNull(keychain.loadPassphrase("nonExistingPassword"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
Copyright (c) 2014 Markus Kreusch
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - log4j config for WebDAV unit tests
|
||||
-->
|
||||
<Configuration status="WARN">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||
</Console>
|
||||
<Console name="StdErr" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<!-- show our own debug messages: -->
|
||||
<Logger name="org.cryptomator" level="DEBUG" />
|
||||
<!-- mute dependencies: -->
|
||||
<Root level="INFO">
|
||||
<AppenderRef ref="Console" />
|
||||
<AppenderRef ref="StdErr" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.5.2</version>
|
||||
<version>1.5.12</version>
|
||||
</parent>
|
||||
<artifactId>launcher</artifactId>
|
||||
<name>Cryptomator Launcher</name>
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.util.concurrent.CountDownLatch;
|
||||
|
||||
@Module
|
||||
class CryptomatorModule {
|
||||
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("shutdownLatch")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved.
|
||||
*
|
||||
*
|
||||
* This class is licensed under the LGPL 3.0 (https://www.gnu.org/licenses/lgpl-3.0.de.html).
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.launcher;
|
||||
@@ -25,7 +25,6 @@ import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Singleton
|
||||
class FileOpenRequestHandler {
|
||||
|
||||
@@ -10,7 +10,6 @@ import javax.inject.Singleton;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Singleton
|
||||
class IpcProtocolImpl implements IpcProtocol {
|
||||
|
||||
@@ -8,12 +8,12 @@ package org.cryptomator.logging;
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import java.util.Map;
|
||||
|
||||
@Singleton
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user