Merge branch 'release/1.6.0'
This commit is contained in:
commit
7e5d64fe6a
@ -2,4 +2,7 @@ escape_special_characters: 0
|
|||||||
commit_message: '[ci skip]'
|
commit_message: '[ci skip]'
|
||||||
files:
|
files:
|
||||||
- source: /presentation/src/main/res/values/strings.xml
|
- source: /presentation/src/main/res/values/strings.xml
|
||||||
translation: /presentation/src/main/res/values-%two_letters_code%/strings.xml
|
translation: /presentation/src/main/res/values-%android_code%/%original_file_name%
|
||||||
|
skip_untranslated_strings: true
|
||||||
|
skip_untranslated_files: false
|
||||||
|
export_only_approved: false
|
||||||
|
51
.github/ISSUE_TEMPLATE/bug.md
vendored
51
.github/ISSUE_TEMPLATE/bug.md
vendored
@ -1,51 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Bug Report"
|
|
||||||
about: "Create a report to help us improve"
|
|
||||||
labels: type:bug
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Please make sure to:
|
|
||||||
- Comply with our code of conduct: https://github.com/cryptomator/android/blob/develop/.github/CODE_OF_CONDUCT.md
|
|
||||||
- Search for existing similar issues first: https://github.com/cryptomator/android/issues?q=
|
|
||||||
|
|
||||||
⚠️ IMPORTANT: If you don't stick to this template, the issue will get closed.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
[Summarize your problem.]
|
|
||||||
|
|
||||||
### System Setup
|
|
||||||
|
|
||||||
* Android version: [Shown in the settings of Android]
|
|
||||||
* Cryptomator version: [Shown in the settings of Cryptomator]
|
|
||||||
* Cloud type: [Dropbox/Google Drive/OneDrive/pCloud/WebDAV/S3/Local storage]
|
|
||||||
|
|
||||||
### Steps to Reproduce
|
|
||||||
|
|
||||||
1. [First step]
|
|
||||||
2. [Second step]
|
|
||||||
3. [and so on…]
|
|
||||||
|
|
||||||
#### Expected Behavior
|
|
||||||
|
|
||||||
[What you expect to happen.]
|
|
||||||
|
|
||||||
#### Actual Behavior
|
|
||||||
|
|
||||||
[What actually happens.]
|
|
||||||
|
|
||||||
#### Reproducibility
|
|
||||||
|
|
||||||
[Always/Intermittent/Only once]
|
|
||||||
|
|
||||||
### Additional Information
|
|
||||||
|
|
||||||
[Any additional information, log files, screenshots or a movie while reproducing the problem, configuration, or data that might be necessary to reproduce the issue.]
|
|
||||||
|
|
||||||
<!--
|
|
||||||
|
|
||||||
If you want to add the log file enable debug mode, reproduce the problem and send it to us: https://community.cryptomator.org/t/how-do-i-enable-debug-mode-on-android/66
|
|
||||||
|
|
||||||
-->
|
|
95
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
95
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
labels: ["type:bug"]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Please agree to the following
|
||||||
|
options:
|
||||||
|
- label: I have searched [existing issues](https://github.com/cryptomator/android/issues?q=) for duplicates
|
||||||
|
required: true
|
||||||
|
- label: I agree to follow this project's [Code of Conduct](https://github.com/cryptomator/android/blob/develop/.github/CODE_OF_CONDUCT.md)
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: summary
|
||||||
|
attributes:
|
||||||
|
label: Summary
|
||||||
|
placeholder: Please summarize your problem.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: software-versions
|
||||||
|
attributes:
|
||||||
|
label: System Setup
|
||||||
|
description: |
|
||||||
|
What software is involved? Please provide version numbers as well.
|
||||||
|
value: |
|
||||||
|
- Android: [Version shown in the settings of Android"]
|
||||||
|
- Cryptomator: [Version shown in the settings of Cryptomator]
|
||||||
|
- …
|
||||||
|
render: markdown
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: cloud-type
|
||||||
|
attributes:
|
||||||
|
label: Cloud Type
|
||||||
|
description: Where is your vault located?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Dropbox
|
||||||
|
- Google Drive
|
||||||
|
- OneDrive
|
||||||
|
- WebDAV
|
||||||
|
- pCloud
|
||||||
|
- S3
|
||||||
|
- Local storage
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction-steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
value: |
|
||||||
|
1. [First Step]
|
||||||
|
2. [Second Step]
|
||||||
|
3. …
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behaviour
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
placeholder: What you expect to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behaviour
|
||||||
|
attributes:
|
||||||
|
label: Actual Behavior
|
||||||
|
placeholder: What actually happens.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: reproducibility
|
||||||
|
attributes:
|
||||||
|
label: Reproducibility
|
||||||
|
description: How often does the described behaviour occur?
|
||||||
|
options:
|
||||||
|
- Always
|
||||||
|
- Intermittent
|
||||||
|
- Only once
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant Log Output
|
||||||
|
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: shell
|
||||||
|
- type: textarea
|
||||||
|
id: further-info
|
||||||
|
attributes:
|
||||||
|
label: Anything else?
|
||||||
|
description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue?
|
28
.github/ISSUE_TEMPLATE/feature.md
vendored
28
.github/ISSUE_TEMPLATE/feature.md
vendored
@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Feature Request"
|
|
||||||
about: "Suggest an idea for this project"
|
|
||||||
labels: type:feature-request
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Please make sure to:
|
|
||||||
- Comply with our code of conduct: https://github.com/cryptomator/android/blob/develop/.github/CODE_OF_CONDUCT.md
|
|
||||||
- Search for existing similar issues first: https://github.com/cryptomator/android/issues?q=
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
|
|
||||||
[One paragraph explanation of the feature.]
|
|
||||||
|
|
||||||
### Motivation
|
|
||||||
|
|
||||||
[Why are we doing this? What use cases does it support? What is the expected outcome?]
|
|
||||||
|
|
||||||
### Considered Alternatives
|
|
||||||
|
|
||||||
[A clear and concise description of the alternative solutions you've considered.]
|
|
||||||
|
|
||||||
### Additional Context
|
|
||||||
|
|
||||||
[Add any other context or screenshots about the feature request here.]
|
|
37
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
37
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
labels: ["type:feature-request"]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Please agree to the following
|
||||||
|
options:
|
||||||
|
- label: I have searched [existing issues](https://github.com/cryptomator/android/issues?q=) for duplicates
|
||||||
|
required: true
|
||||||
|
- label: I agree to follow this project's [Code of Conduct](https://github.com/cryptomator/android/blob/develop/.github/CODE_OF_CONDUCT.md)
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: summary
|
||||||
|
attributes:
|
||||||
|
label: Summary
|
||||||
|
placeholder: Please summarize your feature request.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: motivation
|
||||||
|
attributes:
|
||||||
|
label: Motivation
|
||||||
|
description: Who requires this feature? What problem does the user face? How would this feature solve the problem?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Considered Alternatives
|
||||||
|
description: What current alternatives or workarounds have you considered? Is there a different way to solve the same problem?
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Anything else?
|
||||||
|
description: Any context, suggestions, screenshots, or concepts you want to share?
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -15,6 +15,6 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-java@v1
|
- uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 1.8
|
java-version: 11
|
||||||
- name: Build and Test
|
- name: Build and Test
|
||||||
run: bash ./gradlew clean test --stacktrace
|
run: bash ./gradlew clean test --stacktrace
|
||||||
|
38
.github/workflows/triageBugs.yml
vendored
38
.github/workflows/triageBugs.yml
vendored
@ -1,38 +0,0 @@
|
|||||||
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
@ -49,3 +49,6 @@ local.properties
|
|||||||
# fdroid
|
# fdroid
|
||||||
**/fastlane/repo/**
|
**/fastlane/repo/**
|
||||||
**/fastlane/tmp/**
|
**/fastlane/tmp/**
|
||||||
|
**/fastlane/izzyscript/iod-scan-apk.php
|
||||||
|
**/fastlane/izzyscript/current_iod-scan-apk.php
|
||||||
|
**/fastlane/izzyscript/current_result_*.json
|
||||||
|
8
.gitmodules
vendored
8
.gitmodules
vendored
@ -1,9 +1,9 @@
|
|||||||
[submodule "msa-auth-for-android"]
|
[submodule "msa-auth-for-android"]
|
||||||
path = msa-auth-for-android
|
path = lib/msa-auth-for-android
|
||||||
url = https://github.com/SailReal/msa-auth-for-android.git
|
url = https://github.com/SailReal/msa-auth-for-android.git
|
||||||
[submodule "subsampling-scale-image-view"]
|
[submodule "subsampling-scale-image-view"]
|
||||||
path = subsampling-scale-image-view
|
path = lib/subsampling-scale-image-view
|
||||||
url = https://github.com/SailReal/subsampling-scale-image-view.git
|
url = https://github.com/SailReal/subsampling-scale-image-view.git
|
||||||
[submodule "pcloud-sdk-java"]
|
[submodule "pcloud-sdk-java"]
|
||||||
path = pcloud-sdk-java
|
path = lib/pcloud-sdk-java
|
||||||
url = https://github.com/SailReal/pcloud-sdk-java
|
url = https://github.com/SailReal/pcloud-sdk-java.git
|
||||||
|
2
.idea/codeStyles/Project.xml
generated
2
.idea/codeStyles/Project.xml
generated
@ -31,6 +31,7 @@
|
|||||||
</option>
|
</option>
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="999" />
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="999" />
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="999" />
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="999" />
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
<Properties>
|
<Properties>
|
||||||
<option name="KEEP_BLANK_LINES" value="true" />
|
<option name="KEEP_BLANK_LINES" value="true" />
|
||||||
@ -178,6 +179,7 @@
|
|||||||
</arrangement>
|
</arrangement>
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
<codeStyleSettings language="kotlin">
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
|
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
<option name="USE_TAB_CHARACTER" value="true" />
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -45,7 +45,7 @@
|
|||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="JDK" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
1
.idea/runConfigurations.xml
generated
1
.idea/runConfigurations.xml
generated
@ -3,6 +3,7 @@
|
|||||||
<component name="RunConfigurationProducerService">
|
<component name="RunConfigurationProducerService">
|
||||||
<option name="ignoredProducers">
|
<option name="ignoredProducers">
|
||||||
<set>
|
<set>
|
||||||
|
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||||
|
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@ -2,8 +2,8 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/msa-auth-for-android" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/lib/msa-auth-for-android" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/pcloud-sdk-java" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/lib/pcloud-sdk-java" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/subsampling-scale-image-view" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/lib/subsampling-scale-image-view" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
150
Gemfile.lock
150
Gemfile.lock
@ -1,65 +1,80 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.3)
|
CFPropertyList (3.0.4)
|
||||||
addressable (2.7.0)
|
rexml
|
||||||
|
addressable (2.8.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
apktools (0.7.4)
|
apktools (0.7.4)
|
||||||
rubyzip (~> 2.0)
|
rubyzip (~> 2.0)
|
||||||
artifactory (3.0.15)
|
artifactory (3.0.15)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.1.1)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.444.0)
|
aws-partitions (1.509.0)
|
||||||
aws-sdk-core (3.114.0)
|
aws-sdk-core (3.121.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.43.0)
|
aws-sdk-kms (1.48.0)
|
||||||
aws-sdk-core (~> 3, >= 3.112.0)
|
aws-sdk-core (~> 3, >= 3.120.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.93.1)
|
aws-sdk-s3 (1.103.0)
|
||||||
aws-sdk-core (~> 3, >= 3.112.0)
|
aws-sdk-core (~> 3, >= 3.120.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.2.3)
|
aws-sigv4 (1.4.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
bcrypt_pbkdf (1.0.1)
|
bcrypt_pbkdf (1.1.0)
|
||||||
claide (1.0.3)
|
claide (1.0.3)
|
||||||
colored (1.2)
|
colored (1.2)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
commander-fastlane (4.4.6)
|
commander (4.6.0)
|
||||||
highline (~> 1.7.2)
|
highline (~> 2.0.0)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
digest-crc (0.6.3)
|
digest-crc (0.6.4)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.7.6)
|
dotenv (2.7.6)
|
||||||
ed25519 (1.2.4)
|
ed25519 (1.2.4)
|
||||||
emoji_regex (3.2.2)
|
emoji_regex (3.2.3)
|
||||||
excon (0.80.0)
|
excon (0.86.0)
|
||||||
faraday (1.3.0)
|
faraday (1.8.0)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0.1)
|
||||||
faraday-net_http (~> 1.0)
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.1)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
ruby2_keywords
|
ruby2_keywords (>= 0.0.4)
|
||||||
faraday-cookie_jar (0.0.7)
|
faraday-cookie_jar (0.0.7)
|
||||||
faraday (>= 0.8.0)
|
faraday (>= 0.8.0)
|
||||||
http-cookie (~> 1.0.0)
|
http-cookie (~> 1.0.0)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.0)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
faraday-net_http (1.0.1)
|
faraday-net_http (1.0.1)
|
||||||
faraday_middleware (1.0.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
|
faraday_middleware (1.1.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.2.3)
|
fastimage (2.2.5)
|
||||||
fastlane (2.180.1)
|
fastlane (2.195.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
addressable (>= 2.3, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
aws-sdk-s3 (~> 1.0)
|
aws-sdk-s3 (~> 1.0)
|
||||||
babosa (>= 1.0.3, < 2.0.0)
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
bundler (>= 1.12.0, < 3.0.0)
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
colored
|
colored
|
||||||
commander-fastlane (>= 4.4.6, < 5.0.0)
|
commander (~> 4.6)
|
||||||
dotenv (>= 2.1.1, < 3.0.0)
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
emoji_regex (>= 0.1, < 4.0)
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
excon (>= 0.71.0, < 1.0.0)
|
excon (>= 0.71.0, < 1.0.0)
|
||||||
@ -68,19 +83,20 @@ GEM
|
|||||||
faraday_middleware (~> 1.0)
|
faraday_middleware (~> 1.0)
|
||||||
fastimage (>= 2.1.0, < 3.0.0)
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
google-api-client (>= 0.37.0, < 0.39.0)
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
google-cloud-storage (>= 1.15.0, < 2.0.0)
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
highline (>= 1.7.2, < 2.0.0)
|
google-cloud-storage (~> 1.31)
|
||||||
|
highline (~> 2.0)
|
||||||
json (< 3.0.0)
|
json (< 3.0.0)
|
||||||
jwt (>= 2.1.0, < 3)
|
jwt (>= 2.1.0, < 3)
|
||||||
mini_magick (>= 4.9.4, < 5.0.0)
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
multipart-post (~> 2.0.0)
|
multipart-post (~> 2.0.0)
|
||||||
naturally (~> 2.2)
|
naturally (~> 2.2)
|
||||||
|
optparse (~> 0.1.1)
|
||||||
plist (>= 3.1.0, < 4.0.0)
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
rubyzip (>= 2.0.0, < 3.0.0)
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
security (= 0.1.3)
|
security (= 0.1.3)
|
||||||
simctl (~> 1.6.3)
|
simctl (~> 1.6.3)
|
||||||
slack-notifier (>= 2.0.0, < 3.0.0)
|
|
||||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
terminal-table (>= 1.4.5, < 2.0.0)
|
terminal-table (>= 1.4.5, < 2.0.0)
|
||||||
tty-screen (>= 0.6.3, < 1.0.0)
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
@ -89,79 +105,75 @@ GEM
|
|||||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
xcpretty (~> 0.3.0)
|
xcpretty (~> 0.3.0)
|
||||||
xcpretty-travis-formatter (>= 0.0.3)
|
xcpretty-travis-formatter (>= 0.0.3)
|
||||||
fastlane-plugin-aws_s3 (2.0.2)
|
fastlane-plugin-aws_s3 (2.0.3)
|
||||||
apktools (~> 0.7)
|
apktools (~> 0.7)
|
||||||
aws-sdk-s3 (~> 1)
|
aws-sdk-s3 (~> 1)
|
||||||
mime-types (~> 3.3)
|
mime-types (~> 3.3)
|
||||||
fastlane-plugin-get_version_name (0.2.2)
|
fastlane-plugin-get_version_name (0.2.2)
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-api-client (0.38.0)
|
google-apis-androidpublisher_v3 (0.11.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
|
google-apis-core (0.4.1)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (~> 0.9)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.0)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.0)
|
retriable (>= 2.0, < 4.a)
|
||||||
signet (~> 0.12)
|
|
||||||
google-apis-core (0.3.0)
|
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
|
||||||
googleauth (~> 0.14)
|
|
||||||
httpclient (>= 2.8.1, < 3.0)
|
|
||||||
mini_mime (~> 1.0)
|
|
||||||
representable (~> 3.0)
|
|
||||||
retriable (>= 2.0, < 4.0)
|
|
||||||
rexml
|
rexml
|
||||||
signet (~> 0.14)
|
|
||||||
webrick
|
webrick
|
||||||
google-apis-iamcredentials_v1 (0.3.0)
|
google-apis-iamcredentials_v1 (0.7.0)
|
||||||
google-apis-core (~> 0.1)
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
google-apis-storage_v1 (0.3.0)
|
google-apis-playcustomapp_v1 (0.5.0)
|
||||||
google-apis-core (~> 0.1)
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
|
google-apis-storage_v1 (0.8.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
google-cloud-core (1.6.0)
|
google-cloud-core (1.6.0)
|
||||||
google-cloud-env (~> 1.0)
|
google-cloud-env (~> 1.0)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.5.0)
|
google-cloud-env (1.5.0)
|
||||||
faraday (>= 0.17.3, < 2.0)
|
faraday (>= 0.17.3, < 2.0)
|
||||||
google-cloud-errors (1.1.0)
|
google-cloud-errors (1.2.0)
|
||||||
google-cloud-storage (1.31.0)
|
google-cloud-storage (1.34.1)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
google-apis-storage_v1 (~> 0.1)
|
google-apis-storage_v1 (~> 0.1)
|
||||||
google-cloud-core (~> 1.2)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (~> 0.9)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (0.16.1)
|
googleauth (1.0.0)
|
||||||
faraday (>= 0.17.3, < 2.0)
|
faraday (>= 0.17.3, < 2.0)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
memoist (~> 0.16)
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (~> 0.14)
|
signet (>= 0.16, < 2.a)
|
||||||
highline (1.7.10)
|
highline (2.0.3)
|
||||||
http-cookie (1.0.3)
|
http-cookie (1.0.4)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
jmespath (1.4.0)
|
jmespath (1.4.0)
|
||||||
json (2.5.1)
|
json (2.5.1)
|
||||||
jwt (2.2.2)
|
jwt (2.2.3)
|
||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
mime-types (3.3.1)
|
mime-types (3.3.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2021.0225)
|
mime-types-data (3.2021.0704)
|
||||||
mini_magick (4.11.0)
|
mini_magick (4.11.0)
|
||||||
mini_mime (1.1.0)
|
mini_mime (1.1.1)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.0.0)
|
||||||
nanaimo (0.3.0)
|
nanaimo (0.3.0)
|
||||||
naturally (2.2.1)
|
naturally (2.2.1)
|
||||||
net-sftp (2.1.2)
|
net-sftp (3.0.0)
|
||||||
net-ssh (>= 2.6.5)
|
net-ssh (>= 5.0.0, < 7.0.0)
|
||||||
net-ssh (5.2.0)
|
net-ssh (6.1.0)
|
||||||
|
optparse (0.1.1)
|
||||||
os (1.1.1)
|
os (1.1.1)
|
||||||
plist (3.6.0)
|
plist (3.6.0)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
rake (13.0.3)
|
rake (13.0.6)
|
||||||
representable (3.1.1)
|
representable (3.1.1)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
@ -169,18 +181,17 @@ GEM
|
|||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
rouge (2.0.7)
|
rouge (2.0.7)
|
||||||
ruby2_keywords (0.0.4)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.0)
|
rubyzip (2.3.2)
|
||||||
security (0.1.3)
|
security (0.1.3)
|
||||||
signet (0.15.0)
|
signet (0.16.0)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.3, < 2.0)
|
faraday (>= 0.17.3, < 2.0)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
multi_json (~> 1.10)
|
multi_json (~> 1.10)
|
||||||
simctl (1.6.8)
|
simctl (1.6.8)
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
slack-notifier (2.3.2)
|
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (1.8.0)
|
terminal-table (1.8.0)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
@ -192,16 +203,17 @@ GEM
|
|||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.7)
|
unf_ext (0.0.8)
|
||||||
unicode-display_width (1.7.0)
|
unicode-display_width (1.8.0)
|
||||||
webrick (1.7.0)
|
webrick (1.7.0)
|
||||||
word_wrap (1.0.0)
|
word_wrap (1.0.0)
|
||||||
xcodeproj (1.19.0)
|
xcodeproj (1.21.0)
|
||||||
CFPropertyList (>= 2.3.3, < 4.0)
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
atomos (~> 0.1.3)
|
atomos (~> 0.1.3)
|
||||||
claide (>= 1.0.2, < 2.0)
|
claide (>= 1.0.2, < 2.0)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
nanaimo (~> 0.3.0)
|
nanaimo (~> 0.3.0)
|
||||||
|
rexml (~> 3.2.4)
|
||||||
xcpretty (0.3.0)
|
xcpretty (0.3.0)
|
||||||
rouge (~> 2.0.7)
|
rouge (~> 2.0.7)
|
||||||
xcpretty-travis-formatter (1.0.1)
|
xcpretty-travis-formatter (1.0.1)
|
||||||
|
41
README.md
41
README.md
@ -3,7 +3,7 @@
|
|||||||
[](http://twitter.com/Cryptomator)
|
[](http://twitter.com/Cryptomator)
|
||||||
[](https://community.cryptomator.org)
|
[](https://community.cryptomator.org)
|
||||||
[](https://docs.cryptomator.org)
|
[](https://docs.cryptomator.org)
|
||||||
[](https://crowdin.com/project/cryptomator-android)
|
[](https://translate.cryptomator.org/)
|
||||||
|
|
||||||
Cryptomator offers multi-platform transparent client-side encryption of your files in the cloud.
|
Cryptomator offers multi-platform transparent client-side encryption of your files in the cloud.
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ Cryptomator for Android is currently available in the following distribution ch
|
|||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
* Git
|
* Git
|
||||||
* JDK 8
|
* JDK 11
|
||||||
* Gradle
|
* Gradle
|
||||||
|
|
||||||
### Run Git and Gradle
|
### Run Git and Gradle
|
||||||
@ -45,43 +45,6 @@ Please make sure before creating a PR, to apply the code style by executing refo
|
|||||||
|
|
||||||
Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](.github/CODE_OF_CONDUCT.md).
|
Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](.github/CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
Follow these steps to deploy a release:
|
|
||||||
|
|
||||||
1. Check `TODO`/`FIXME` comments
|
|
||||||
- Create issue for or delete
|
|
||||||
- Regexp for "Find in Path": `\W(TODO|FIXME)(?! #[0-9]{1,4}:)`
|
|
||||||
1. Merge translations
|
|
||||||
1. Check latest dependencies
|
|
||||||
1. Create release branch
|
|
||||||
1. Test database migration
|
|
||||||
1. Smoke-Test changed or added functionality
|
|
||||||
1. Update version
|
|
||||||
1. Create and commit release notes
|
|
||||||
1. Merge in `main`
|
|
||||||
1. Create tag and execute deploy app using Fastlane
|
|
||||||
1. Close GitHub-issues or move them to next milestone
|
|
||||||
1. Close milestone
|
|
||||||
1. Update version on website ([cryptomator.org/android](https://cryptomator.org/android/))
|
|
||||||
|
|
||||||
### Release Notes
|
|
||||||
|
|
||||||
Before tagging the release, create and commit the release notes. For Playstore create [fastlane/metadata/android/de-DE/changelogs/default.txt](https://github.com/cryptomator/android/blob/develop/fastlane/metadata/android/de-DE/changelogs/default.txt), [fastlane/metadata/android/en-US/changelogs/default.txt](https://github.com/cryptomator/android/blob/develop/fastlane/metadata/android/en-US/changelogs/default.txt) and for the website create [fastlane/release-notes.html](https://github.com/cryptomator/android/blob/develop/fastlane/release-notes.html).
|
|
||||||
|
|
||||||
### Deploy app using Fastlane
|
|
||||||
|
|
||||||
Deploy production version to Google Play, Website/GitHub-Releases and F-Droid using `fastlane android deploy` or `bundle exec fastlane deploy`
|
|
||||||
|
|
||||||
There are further targets and options like `beta`, see [fastlane/README.md](https://github.com/cryptomator/android/blob/develop/fastlane/README.md)
|
|
||||||
|
|
||||||
### Initial setup Fastlane
|
|
||||||
|
|
||||||
1. Make sure you copied `.default.env` to `.env` in the `fastlane` folder and filled out those variables.
|
|
||||||
1. Install Ruby (depends on OS, Ubuntu): `sudo apt install ruby-dev`
|
|
||||||
1. Install fastlane (depends on OS, Ubuntu): `gem install fastlane -N`
|
|
||||||
1. Install `fdroidserver` using `apt`, `pacman`, ..., see https://f-droid.org/docs/Installing_the_Server_and_Repo_Tools/
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is dual-licensed under the GPLv3 for FOSS projects as well as a commercial license for independent software vendors and resellers. If you want to modify this application under different conditions, feel free to contact our support team.
|
This project is dual-licensed under the GPLv3 for FOSS projects as well as a commercial license for independent software vendors and resellers. If you want to modify this application under different conditions, feel free to contact our support team.
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
apply from: 'buildsystem/ci.gradle'
|
|
||||||
apply from: 'buildsystem/dependencies.gradle'
|
apply from: 'buildsystem/dependencies.gradle'
|
||||||
apply plugin: "com.vanniktech.android.junit.jacoco"
|
apply plugin: "com.vanniktech.android.junit.jacoco"
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.4.32'
|
ext.kotlin_version = '1.5.31'
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
classpath 'com.android.tools.build:gradle:7.0.2'
|
||||||
classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0'
|
classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0'
|
||||||
classpath 'com.fernandocejas.frodo:frodo-plugin:0.8.3'
|
|
||||||
classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0'
|
classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.7.1.1"
|
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.7.1.1"
|
||||||
@ -42,7 +39,7 @@ allprojects {
|
|||||||
ext {
|
ext {
|
||||||
androidApplicationId = 'org.cryptomator'
|
androidApplicationId = 'org.cryptomator'
|
||||||
androidVersionCode = getVersionCode()
|
androidVersionCode = getVersionCode()
|
||||||
androidVersionName = '1.5.18'
|
androidVersionName = '1.6.0'
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
def ciServer = 'TRAVIS'
|
|
||||||
def executingOnCI = "true" == System.getenv(ciServer)
|
|
||||||
|
|
||||||
// Since for CI we always do full clean builds, we don't want to pre-dex
|
|
||||||
// See http://tools.android.com/tech-docs/new-build-system/tips
|
|
||||||
subprojects {
|
|
||||||
project.plugins.whenPluginAdded { plugin ->
|
|
||||||
if ('com.android.build.gradle.AppPlugin' == plugin.class.name ||
|
|
||||||
'com.android.build.gradle.LibraryPlugin' == plugin.class.name) {
|
|
||||||
project.android.dexOptions.preDexLibraries = !executingOnCI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,13 @@
|
|||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
mavenCentral()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
androidBuildToolsVersion = "29.0.2"
|
androidBuildToolsVersion = "30.0.2"
|
||||||
androidMinSdkVersion = 23
|
androidMinSdkVersion = 24
|
||||||
androidTargetSdkVersion = 29
|
androidTargetSdkVersion = 29
|
||||||
androidCompileSdkVersion = 29
|
androidCompileSdkVersion = 29
|
||||||
|
|
||||||
@ -17,8 +18,10 @@ ext {
|
|||||||
|
|
||||||
// support lib
|
// support lib
|
||||||
androidSupportAnnotationsVersion = '1.2.0'
|
androidSupportAnnotationsVersion = '1.2.0'
|
||||||
androidSupportAppcompatVersion = '1.2.0'
|
androidSupportAppcompatVersion = '1.3.1'
|
||||||
androidSupportDesignVersion = '1.3.0'
|
androidSupportDesignVersion = '1.4.0'
|
||||||
|
|
||||||
|
coreDesugaringVersion = '1.1.5'
|
||||||
|
|
||||||
// app frameworks and utilities
|
// app frameworks and utilities
|
||||||
|
|
||||||
@ -26,70 +29,73 @@ ext {
|
|||||||
rxAndroidVersion = '2.1.1'
|
rxAndroidVersion = '2.1.1'
|
||||||
rxBindingVersion = '2.2.0'
|
rxBindingVersion = '2.2.0'
|
||||||
|
|
||||||
daggerVersion = '2.35'
|
daggerVersion = '2.39'
|
||||||
|
|
||||||
gsonVersion = '2.8.6'
|
gsonVersion = '2.8.8'
|
||||||
|
|
||||||
okHttpVersion = '4.9.1'
|
okHttpVersion = '4.9.2'
|
||||||
okHttpDigestVersion = '2.5'
|
okHttpDigestVersion = '2.5'
|
||||||
|
|
||||||
velocityVersion = '1.7'
|
velocityVersion = '2.3'
|
||||||
|
|
||||||
timberVersion = '4.7.1'
|
timberVersion = '5.0.1'
|
||||||
|
|
||||||
zxcvbnVersion = '1.5.0'
|
zxcvbnVersion = '1.5.2'
|
||||||
|
|
||||||
scaleImageViewVersion = '3.10.0'
|
scaleImageViewVersion = '3.10.0'
|
||||||
|
|
||||||
lruFileCacheVersion = '1.0'
|
lruFileCacheVersion = '1.2'
|
||||||
|
|
||||||
// KEEP IN SYNC WITH GENERATOR VERSION IN root build.gradle
|
// KEEP IN SYNC WITH GENERATOR VERSION IN root build.gradle
|
||||||
greenDaoVersion = '3.3.0'
|
greenDaoVersion = '3.3.0'
|
||||||
|
|
||||||
// cloud provider libs
|
// cloud provider libs
|
||||||
|
cryptolibVersion = '2.0.2'
|
||||||
|
|
||||||
// do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x
|
dropboxVersion = '4.0.1'
|
||||||
cryptolibVersion = '1.3.0'
|
|
||||||
|
|
||||||
awsAndroidSdkS3 = '2.23.0'
|
googleApiServicesVersion = 'v3-rev20210919-1.32.1'
|
||||||
|
googlePlayServicesVersion = '19.2.0'
|
||||||
dropboxVersion = '4.0.0'
|
googleClientVersion = '1.32.1' // keep in sync with https://github.com/SailReal/google-http-java-client
|
||||||
|
/*
|
||||||
googleApiServicesVersion = 'v3-rev197-1.25.0'
|
update using https://github.com/SailReal/google-http-java-client with `mvn clean install`,
|
||||||
googlePlayServicesVersion = '19.0.0'
|
copying `google-http-client-*.jar` and `google-http-client-android-*.jar` into the lib folder of this project
|
||||||
googleClientVersion = '1.31.4'
|
*/
|
||||||
|
trackingFreeGoogleCLientVersion = '1.40.1'
|
||||||
|
|
||||||
msgraphVersion = '2.10.0'
|
msgraphVersion = '2.10.0'
|
||||||
|
|
||||||
|
minIoVersion = '8.3.0'
|
||||||
|
staxVersion = '1.2.0' // needed for minIO
|
||||||
|
|
||||||
commonsCodecVersion = '1.15'
|
commonsCodecVersion = '1.15'
|
||||||
|
|
||||||
recyclerViewFastScrollVersion = '2.0.1'
|
recyclerViewFastScrollVersion = '2.0.1'
|
||||||
|
|
||||||
// testing dependencies
|
// testing dependencies
|
||||||
|
|
||||||
jUnitVersion = '5.7.1'
|
jUnitVersion = '5.8.1'
|
||||||
jUnit4Version = '4.13.1'
|
|
||||||
assertJVersion = '1.7.1'
|
assertJVersion = '1.7.1'
|
||||||
mockitoVersion = '3.9.0'
|
mockitoVersion = '3.12.4'
|
||||||
mockitoInlineVersion = '3.9.0'
|
mockitoKotlinVersion = '3.2.0'
|
||||||
hamcrestVersion = '1.3'
|
hamcrestVersion = '1.3'
|
||||||
dexmakerVersion = '1.0'
|
dexmakerVersion = '1.0'
|
||||||
espressoVersion = '3.3.0'
|
espressoVersion = '3.4.0'
|
||||||
testingSupportLibVersion = '0.1'
|
testingSupportLibVersion = '0.1'
|
||||||
runnerVersion = '1.3.0'
|
runnerVersion = '1.4.0'
|
||||||
rulesVersion = '1.3.0'
|
rulesVersion = '1.4.0'
|
||||||
contributionVersion = '3.3.0'
|
contributionVersion = '3.4.0'
|
||||||
uiautomatorVersion = '2.2.0'
|
uiautomatorVersion = '2.2.0'
|
||||||
|
|
||||||
androidxCoreVersion = '1.3.2'
|
androidxCoreVersion = '1.6.0'
|
||||||
androidxFragmentVersion = '1.3.2'
|
androidxFragmentVersion = '1.3.6'
|
||||||
androidxViewpagerVersion = '1.0.0'
|
androidxViewpagerVersion = '1.0.0'
|
||||||
androidxSwiperefreshVersion = '1.1.0'
|
androidxSwiperefreshVersion = '1.1.0'
|
||||||
androidxPreferenceVersion = '1.0.0' // 1.1.0 and 1.1.2 does have a bug with the text size
|
androidxPreferenceVersion = '1.1.1'
|
||||||
androidxRecyclerViewVersion = '1.2.0'
|
androidxRecyclerViewVersion = '1.2.1'
|
||||||
androidxDocumentfileVersion = '1.0.1'
|
androidxDocumentfileVersion = '1.0.1'
|
||||||
androidxBiometricVersion = '1.1.0'
|
androidxBiometricVersion = '1.1.0'
|
||||||
androidxTestCoreVersion = '1.3.0'
|
androidxTestCoreVersion = '1.4.0'
|
||||||
|
|
||||||
jsonWebTokenApiVersion = '0.11.2'
|
jsonWebTokenApiVersion = '0.11.2'
|
||||||
|
|
||||||
@ -103,7 +109,6 @@ ext {
|
|||||||
androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}",
|
androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}",
|
||||||
androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}",
|
androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}",
|
||||||
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
|
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
|
||||||
awsAndroidS3 : "com.amazonaws:aws-android-sdk-s3:${awsAndroidSdkS3}",
|
|
||||||
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
|
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
|
||||||
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
|
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
|
||||||
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",
|
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",
|
||||||
@ -112,11 +117,14 @@ ext {
|
|||||||
dagger : "com.google.dagger:dagger:${daggerVersion}",
|
dagger : "com.google.dagger:dagger:${daggerVersion}",
|
||||||
daggerCompiler : "com.google.dagger:dagger-compiler:${daggerVersion}",
|
daggerCompiler : "com.google.dagger:dagger-compiler:${daggerVersion}",
|
||||||
design : "com.google.android.material:material:${androidSupportDesignVersion}",
|
design : "com.google.android.material:material:${androidSupportDesignVersion}",
|
||||||
|
coreDesugaring : "com.android.tools:desugar_jdk_libs:${coreDesugaringVersion}",
|
||||||
dropbox : "com.dropbox.core:dropbox-core-sdk:${dropboxVersion}",
|
dropbox : "com.dropbox.core:dropbox-core-sdk:${dropboxVersion}",
|
||||||
espresso : "androidx.test.espresso:espresso-core:${espressoVersion}",
|
espresso : "androidx.test.espresso:espresso-core:${espressoVersion}",
|
||||||
googleApiClientAndroid: "com.google.api-client:google-api-client-android:${googleClientVersion}",
|
googleApiClientAndroid: "com.google.api-client:google-api-client-android:${googleClientVersion}",
|
||||||
googleApiServicesDrive: "com.google.apis:google-api-services-drive:${googleApiServicesVersion}",
|
googleApiServicesDrive: "com.google.apis:google-api-services-drive:${googleApiServicesVersion}",
|
||||||
googlePlayServicesAuth: "com.google.android.gms:play-services-auth:${googlePlayServicesVersion}",
|
googlePlayServicesAuth: "com.google.android.gms:play-services-auth:${googlePlayServicesVersion}",
|
||||||
|
trackingFreeGoogleCLient : files("lib/google-http-client-${trackingFreeGoogleCLientVersion}.jar"),
|
||||||
|
trackingFreeGoogleAndroidCLient: files("lib/google-http-client-android-${trackingFreeGoogleCLientVersion}.jar"),
|
||||||
greenDao : "org.greenrobot:greendao:${greenDaoVersion}",
|
greenDao : "org.greenrobot:greendao:${greenDaoVersion}",
|
||||||
gson : "com.google.code.gson:gson:${gsonVersion}",
|
gson : "com.google.code.gson:gson:${gsonVersion}",
|
||||||
hamcrest : "org.hamcrest:hamcrest-all:${hamcrestVersion}",
|
hamcrest : "org.hamcrest:hamcrest-all:${hamcrestVersion}",
|
||||||
@ -125,28 +133,30 @@ ext {
|
|||||||
junitApi : "org.junit.jupiter:junit-jupiter-api:${jUnitVersion}",
|
junitApi : "org.junit.jupiter:junit-jupiter-api:${jUnitVersion}",
|
||||||
junitEngine : "org.junit.jupiter:junit-jupiter-engine:${jUnitVersion}",
|
junitEngine : "org.junit.jupiter:junit-jupiter-engine:${jUnitVersion}",
|
||||||
junitParams : "org.junit.jupiter:junit-jupiter-params:${jUnitVersion}",
|
junitParams : "org.junit.jupiter:junit-jupiter-params:${jUnitVersion}",
|
||||||
junit4 : "org.junit.jupiter:junit-jupiter:${jUnit4Version}",
|
|
||||||
junit4Engine : "org.junit.vintage:junit-vintage-engine:${jUnitVersion}",
|
junit4Engine : "org.junit.vintage:junit-vintage-engine:${jUnitVersion}",
|
||||||
msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}",
|
minIo : "io.minio:minio:${minIoVersion}",
|
||||||
mockito : "org.mockito:mockito-core:${mockitoVersion}",
|
mockito : "org.mockito:mockito-core:${mockitoVersion}",
|
||||||
mockitoInline : "org.mockito:mockito-inline:${mockitoInlineVersion}",
|
mockitoInline : "org.mockito:mockito-inline:${mockitoVersion}",
|
||||||
|
mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:${mockitoKotlinVersion}",
|
||||||
|
msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}",
|
||||||
multidex : "androidx.multidex:multidex:${multidexVersion}",
|
multidex : "androidx.multidex:multidex:${multidexVersion}",
|
||||||
okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}",
|
okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}",
|
||||||
okHttpDigest : "com.burgstaller:okhttp-digest:${okHttpDigestVersion}",
|
okHttpDigest : "io.github.rburgst:okhttp-digest:${okHttpDigestVersion}",
|
||||||
recyclerViewFastScroll: "com.simplecityapps:recyclerview-fastscroll:${recyclerViewFastScrollVersion}",
|
recyclerViewFastScroll: "com.simplecityapps:recyclerview-fastscroll:${recyclerViewFastScrollVersion}",
|
||||||
rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}",
|
rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}",
|
||||||
rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}",
|
rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}",
|
||||||
rxBinding : "com.jakewharton.rxbinding2:rxbinding:${rxBindingVersion}",
|
rxBinding : "com.jakewharton.rxbinding2:rxbinding:${rxBindingVersion}",
|
||||||
|
stax : "stax:stax:${staxVersion}",
|
||||||
testingSupportLib : "com.android.support.test:testing-support-lib:${testingSupportLibVersion}",
|
testingSupportLib : "com.android.support.test:testing-support-lib:${testingSupportLibVersion}",
|
||||||
timber : "com.jakewharton.timber:timber:${timberVersion}",
|
timber : "com.jakewharton.timber:timber:${timberVersion}",
|
||||||
velocity : "org.apache.velocity:velocity:${velocityVersion}",
|
velocity : "org.apache.velocity:velocity-engine-core:${velocityVersion}",
|
||||||
runner : "androidx.test:runner:${runnerVersion}",
|
runner : "androidx.test:runner:${runnerVersion}",
|
||||||
rules : "androidx.test:rules:${rulesVersion}",
|
rules : "androidx.test:rules:${rulesVersion}",
|
||||||
contribution : "androidx.test.espresso:espresso-contrib:${contributionVersion}",
|
contribution : "androidx.test.espresso:espresso-contrib:${contributionVersion}",
|
||||||
uiAutomator : "androidx.test.uiautomator:uiautomator:${uiautomatorVersion}",
|
uiAutomator : "androidx.test.uiautomator:uiautomator:${uiautomatorVersion}",
|
||||||
zxcvbn : "com.nulab-inc:zxcvbn:${zxcvbnVersion}",
|
zxcvbn : "com.nulab-inc:zxcvbn:${zxcvbnVersion}",
|
||||||
scaleImageView : "com.davemorrissey.labs:subsampling-scale-image-view:${scaleImageViewVersion}",
|
scaleImageView : "com.davemorrissey.labs:subsampling-scale-image-view:${scaleImageViewVersion}",
|
||||||
lruFileCache : "com.tomclaw.cache:cache:${lruFileCacheVersion}",
|
lruFileCache : "com.github.solkin:disk-lru-cache:${lruFileCacheVersion}",
|
||||||
jsonWebTokenApi : "io.jsonwebtoken:jjwt-api:${jsonWebTokenApiVersion}",
|
jsonWebTokenApi : "io.jsonwebtoken:jjwt-api:${jsonWebTokenApiVersion}",
|
||||||
jsonWebTokenImpl : "io.jsonwebtoken:jjwt-impl:${jsonWebTokenApiVersion}",
|
jsonWebTokenImpl : "io.jsonwebtoken:jjwt-impl:${jsonWebTokenApiVersion}",
|
||||||
jsonWebTokenJson : "io.jsonwebtoken:jjwt-orgjson:${jsonWebTokenApiVersion}"
|
jsonWebTokenJson : "io.jsonwebtoken:jjwt-orgjson:${jsonWebTokenApiVersion}"
|
||||||
|
@ -17,11 +17,15 @@ android {
|
|||||||
|
|
||||||
buildConfigField 'int', 'VERSION_CODE', "${globalConfiguration["androidVersionCode"]}"
|
buildConfigField 'int', 'VERSION_CODE', "${globalConfiguration["androidVersionCode"]}"
|
||||||
buildConfigField "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\""
|
buildConfigField "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\""
|
||||||
|
|
||||||
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
@ -71,10 +75,14 @@ android {
|
|||||||
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/']
|
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/DEPENDENCIES'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
greendao {
|
greendao {
|
||||||
schemaVersion 6
|
schemaVersion 9
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.all {
|
configurations.all {
|
||||||
@ -90,6 +98,8 @@ dependencies {
|
|||||||
implementation project(':msa-auth-for-android')
|
implementation project(':msa-auth-for-android')
|
||||||
implementation project(':pcloud-sdk-java')
|
implementation project(':pcloud-sdk-java')
|
||||||
|
|
||||||
|
coreLibraryDesugaring dependencies.coreDesugaring
|
||||||
|
|
||||||
// cryptomator
|
// cryptomator
|
||||||
implementation dependencies.cryptolib
|
implementation dependencies.cryptolib
|
||||||
|
|
||||||
@ -98,32 +108,66 @@ dependencies {
|
|||||||
// dagger
|
// dagger
|
||||||
annotationProcessor dependencies.daggerCompiler
|
annotationProcessor dependencies.daggerCompiler
|
||||||
implementation dependencies.dagger
|
implementation dependencies.dagger
|
||||||
|
|
||||||
|
api dependencies.jsonWebTokenApi
|
||||||
|
implementation dependencies.jsonWebTokenImpl
|
||||||
|
implementation dependencies.jsonWebTokenJson
|
||||||
|
|
||||||
// cloud
|
// cloud
|
||||||
implementation dependencies.awsAndroidS3
|
|
||||||
implementation dependencies.dropbox
|
implementation dependencies.dropbox
|
||||||
implementation dependencies.msgraph
|
implementation dependencies.msgraph
|
||||||
|
|
||||||
playstoreImplementation dependencies.googlePlayServicesAuth
|
implementation dependencies.stax
|
||||||
apkstoreImplementation dependencies.googlePlayServicesAuth
|
api dependencies.minIo
|
||||||
|
|
||||||
|
playstoreImplementation(dependencies.googlePlayServicesAuth) {
|
||||||
|
exclude module: 'guava-jdk5'
|
||||||
|
exclude module: 'httpclient'
|
||||||
|
exclude module: 'googlehttpclient'
|
||||||
|
exclude group: "com.google.http-client", module: "google-http-client"
|
||||||
|
}
|
||||||
|
apkstoreImplementation(dependencies.googlePlayServicesAuth) {
|
||||||
|
exclude module: 'guava-jdk5'
|
||||||
|
exclude module: 'httpclient'
|
||||||
|
exclude module: "google-http-client"
|
||||||
|
exclude group: "com.google.http-client", module: "google-http-client"
|
||||||
|
}
|
||||||
|
|
||||||
playstoreImplementation(dependencies.googleApiServicesDrive) {
|
playstoreImplementation(dependencies.googleApiServicesDrive) {
|
||||||
exclude module: 'guava-jdk5'
|
exclude module: 'guava-jdk5'
|
||||||
exclude module: 'httpclient'
|
exclude module: 'httpclient'
|
||||||
|
exclude module: 'googlehttpclient'
|
||||||
|
exclude group: "com.google.http-client", module: "google-http-client"
|
||||||
}
|
}
|
||||||
apkstoreImplementation(dependencies.googleApiServicesDrive) {
|
apkstoreImplementation(dependencies.googleApiServicesDrive) {
|
||||||
exclude module: 'guava-jdk5'
|
exclude module: 'guava-jdk5'
|
||||||
exclude module: 'httpclient'
|
exclude module: 'httpclient'
|
||||||
|
exclude module: "google-http-client"
|
||||||
|
exclude group: "com.google.http-client", module: "google-http-client"
|
||||||
}
|
}
|
||||||
|
|
||||||
playstoreImplementation(dependencies.googleApiClientAndroid) {
|
playstoreImplementation(dependencies.googleApiClientAndroid) {
|
||||||
exclude module: 'guava-jdk5'
|
exclude module: 'guava-jdk5'
|
||||||
exclude module: 'httpclient'
|
exclude module: 'httpclient'
|
||||||
|
exclude module: "google-http-client"
|
||||||
|
exclude module: "jetified-google-http-client"
|
||||||
|
exclude group: "com.google.http-client", module: "google-http-client"
|
||||||
|
exclude group: "com.google.http-client", module: "jetified-google-http-client"
|
||||||
}
|
}
|
||||||
apkstoreImplementation(dependencies.googleApiClientAndroid) {
|
apkstoreImplementation(dependencies.googleApiClientAndroid) {
|
||||||
exclude module: 'guava-jdk5'
|
exclude module: 'guava-jdk5'
|
||||||
exclude module: 'httpclient'
|
exclude module: 'httpclient'
|
||||||
|
exclude module: "google-http-client"
|
||||||
|
exclude module: "jetified-google-http-client"
|
||||||
|
exclude group: "com.google.http-client", module: "google-http-client"
|
||||||
|
exclude group: "com.google.http-client", module: "jetified-google-http-client"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playstoreImplementation dependencies.trackingFreeGoogleCLient
|
||||||
|
apkstoreImplementation dependencies.trackingFreeGoogleCLient
|
||||||
|
playstoreImplementation dependencies.trackingFreeGoogleAndroidCLient
|
||||||
|
apkstoreImplementation dependencies.trackingFreeGoogleAndroidCLient
|
||||||
|
|
||||||
// rest
|
// rest
|
||||||
implementation dependencies.rxJava
|
implementation dependencies.rxJava
|
||||||
implementation dependencies.rxAndroid
|
implementation dependencies.rxAndroid
|
||||||
@ -146,12 +190,16 @@ dependencies {
|
|||||||
testImplementation dependencies.junitApi
|
testImplementation dependencies.junitApi
|
||||||
testRuntimeOnly dependencies.junitEngine
|
testRuntimeOnly dependencies.junitEngine
|
||||||
testImplementation dependencies.junitParams
|
testImplementation dependencies.junitParams
|
||||||
|
|
||||||
testImplementation dependencies.junit4
|
|
||||||
testRuntimeOnly dependencies.junit4Engine
|
testRuntimeOnly dependencies.junit4Engine
|
||||||
|
|
||||||
testImplementation dependencies.mockito
|
testImplementation dependencies.mockito
|
||||||
|
testImplementation dependencies.mockitoKotlin
|
||||||
|
testImplementation dependencies.mockitoInline
|
||||||
testImplementation dependencies.hamcrest
|
testImplementation dependencies.hamcrest
|
||||||
|
|
||||||
|
androidTestImplementation(dependencies.runner) {
|
||||||
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
@ -161,3 +209,16 @@ configurations {
|
|||||||
static def getApiKey(key) {
|
static def getApiKey(key) {
|
||||||
return System.getenv().getOrDefault(key, "")
|
return System.getenv().getOrDefault(key, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(Test) {
|
||||||
|
testLogging {
|
||||||
|
events "failed"
|
||||||
|
|
||||||
|
showExceptions true
|
||||||
|
exceptionFormat "full"
|
||||||
|
showCauses true
|
||||||
|
showStackTraces true
|
||||||
|
|
||||||
|
showStandardStreams = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,410 @@
|
|||||||
|
package org.cryptomator.data.db
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import androidx.test.InstrumentationRegistry
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import androidx.test.runner.AndroidJUnit4
|
||||||
|
import org.cryptomator.data.db.entities.CloudEntityDao
|
||||||
|
import org.cryptomator.data.db.entities.UpdateCheckEntityDao
|
||||||
|
import org.cryptomator.data.db.entities.VaultEntityDao
|
||||||
|
import org.cryptomator.domain.CloudType
|
||||||
|
import org.cryptomator.util.SharedPreferencesHandler
|
||||||
|
import org.cryptomator.util.crypto.CredentialCryptor
|
||||||
|
import org.greenrobot.greendao.database.Database
|
||||||
|
import org.greenrobot.greendao.database.StandardDatabase
|
||||||
|
import org.greenrobot.greendao.internal.DaoConfig
|
||||||
|
import org.hamcrest.CoreMatchers
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@SmallTest
|
||||||
|
class UpgradeDatabaseTest {
|
||||||
|
|
||||||
|
private val context = InstrumentationRegistry.getTargetContext()
|
||||||
|
private val sharedPreferencesHandler = SharedPreferencesHandler(context)
|
||||||
|
private lateinit var db: Database
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
db = StandardDatabase(SQLiteDatabase.create(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upgradeAll() {
|
||||||
|
Upgrade0To1().applyTo(db, 0)
|
||||||
|
Upgrade1To2().applyTo(db, 1)
|
||||||
|
Upgrade2To3(context).applyTo(db, 2)
|
||||||
|
Upgrade3To4().applyTo(db, 3)
|
||||||
|
Upgrade4To5().applyTo(db, 4)
|
||||||
|
Upgrade5To6().applyTo(db, 5)
|
||||||
|
Upgrade6To7().applyTo(db, 6)
|
||||||
|
Upgrade7To8().applyTo(db, 7)
|
||||||
|
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
|
||||||
|
|
||||||
|
CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll()
|
||||||
|
VaultEntityDao(DaoConfig(db, VaultEntityDao::class.java)).loadAll()
|
||||||
|
UpdateCheckEntityDao(DaoConfig(db, UpdateCheckEntityDao::class.java)).loadAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upgrade2To3() {
|
||||||
|
Upgrade0To1().applyTo(db, 0)
|
||||||
|
Upgrade1To2().applyTo(db, 1)
|
||||||
|
|
||||||
|
val url = "url"
|
||||||
|
val username = "username"
|
||||||
|
val webdavCertificate = "webdavCertificate"
|
||||||
|
val accessToken = "accessToken"
|
||||||
|
|
||||||
|
Sql.update("CLOUD_ENTITY")
|
||||||
|
.where("TYPE", Sql.eq("DROPBOX"))
|
||||||
|
.set("ACCESS_TOKEN", Sql.toString(accessToken))
|
||||||
|
.set("WEBDAV_URL", Sql.toString(url))
|
||||||
|
.set("USERNAME", Sql.toString(username))
|
||||||
|
.set("WEBDAV_CERTIFICATE", Sql.toString(webdavCertificate))
|
||||||
|
.executeOn(db)
|
||||||
|
|
||||||
|
Sql.update("CLOUD_ENTITY")
|
||||||
|
.where("TYPE", Sql.eq("ONEDRIVE"))
|
||||||
|
.set("ACCESS_TOKEN", Sql.toString("NOT USED"))
|
||||||
|
.set("WEBDAV_URL", Sql.toString(url))
|
||||||
|
.set("USERNAME", Sql.toString(username))
|
||||||
|
.set("WEBDAV_CERTIFICATE", Sql.toString(webdavCertificate))
|
||||||
|
.executeOn(db)
|
||||||
|
|
||||||
|
context.getSharedPreferences("com.microsoft.live", Context.MODE_PRIVATE).edit().putString("refresh_token", accessToken).commit()
|
||||||
|
|
||||||
|
Upgrade2To3(context).applyTo(db, 2)
|
||||||
|
|
||||||
|
checkUpgrade2to3ResultForCloud("DROPBOX", accessToken, url, username, webdavCertificate)
|
||||||
|
checkUpgrade2to3ResultForCloud("ONEDRIVE", accessToken, url, username, webdavCertificate)
|
||||||
|
|
||||||
|
Assert.assertThat(context.getSharedPreferences("com.microsoft.live", Context.MODE_PRIVATE).getString("refresh_token", null), CoreMatchers.nullValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkUpgrade2to3ResultForCloud(cloudName: String, accessToken: String, url: String, username: String, webdavCertificate: String) {
|
||||||
|
Sql.query("CLOUD_ENTITY").where("TYPE", Sql.eq(cloudName)).executeOn(db).use {
|
||||||
|
it.moveToFirst()
|
||||||
|
Assert.assertThat(CredentialCryptor.getInstance(context).decrypt(it.getString(it.getColumnIndex("ACCESS_TOKEN"))), CoreMatchers.`is`(accessToken))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("WEBDAV_URL")), CoreMatchers.`is`(url))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("USERNAME")), CoreMatchers.`is`(username))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("WEBDAV_CERTIFICATE")), CoreMatchers.`is`(webdavCertificate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upgrade3To4() {
|
||||||
|
Upgrade0To1().applyTo(db, 0)
|
||||||
|
Upgrade1To2().applyTo(db, 1)
|
||||||
|
Upgrade2To3(context).applyTo(db, 2)
|
||||||
|
|
||||||
|
val ids = arrayOf("10", "20", "31", "32", "51")
|
||||||
|
|
||||||
|
ids.forEach {
|
||||||
|
Sql.insertInto("VAULT_ENTITY") //
|
||||||
|
.integer("_id", Integer.parseInt(it)) //
|
||||||
|
.integer("FOLDER_CLOUD_ID", 1) //
|
||||||
|
.text("FOLDER_PATH", "path${it}") //
|
||||||
|
.text("FOLDER_NAME", "name${it}") //
|
||||||
|
.text("CLOUD_TYPE", CloudType.DROPBOX.name) //
|
||||||
|
.text("PASSWORD", "password${it}") //
|
||||||
|
.executeOn(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
Upgrade3To4().applyTo(db, 3)
|
||||||
|
|
||||||
|
Sql.query("VAULT_ENTITY").where("CLOUD_TYPE", Sql.eq(CloudType.DROPBOX.name)).executeOn(db).use {
|
||||||
|
Assert.assertThat(it.count, CoreMatchers.`is`(ids.size))
|
||||||
|
while (it.moveToNext()) {
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("_id")), CoreMatchers.`is`(ids[it.position]))
|
||||||
|
Assert.assertThat(it.getInt(it.getColumnIndex("POSITION")), CoreMatchers.`is`(it.position))
|
||||||
|
Assert.assertThat(it.getInt(it.getColumnIndex("FOLDER_CLOUD_ID")), CoreMatchers.`is`(1))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_PATH")), CoreMatchers.`is`("path${ids[it.position]}"))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_NAME")), CoreMatchers.`is`("name${ids[it.position]}"))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("CLOUD_TYPE")), CoreMatchers.`is`(CloudType.DROPBOX.name))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("PASSWORD")), CoreMatchers.`is`("password${ids[it.position]}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upgrade4To5() {
|
||||||
|
Upgrade0To1().applyTo(db, 0)
|
||||||
|
Upgrade1To2().applyTo(db, 1)
|
||||||
|
Upgrade2To3(context).applyTo(db, 2)
|
||||||
|
Upgrade3To4().applyTo(db, 3)
|
||||||
|
|
||||||
|
val cloudId = 15
|
||||||
|
val cloudUrl = "url"
|
||||||
|
val username = "username"
|
||||||
|
val webdavCertificate = "webdavCertificate"
|
||||||
|
val accessToken = "accessToken"
|
||||||
|
|
||||||
|
val vaultId = 25
|
||||||
|
val folderPath = "path"
|
||||||
|
val folderName = "name"
|
||||||
|
val password = "password"
|
||||||
|
val position = 10
|
||||||
|
|
||||||
|
Sql.insertInto("CLOUD_ENTITY") //
|
||||||
|
.integer("_id", cloudId) //
|
||||||
|
.text("TYPE", CloudType.WEBDAV.name) //
|
||||||
|
.text("ACCESS_TOKEN", accessToken) //
|
||||||
|
.text("WEBDAV_URL", cloudUrl) //
|
||||||
|
.text("USERNAME", username) //
|
||||||
|
.text("WEBDAV_CERTIFICATE", webdavCertificate) //
|
||||||
|
.executeOn(db)
|
||||||
|
|
||||||
|
Sql.insertInto("VAULT_ENTITY") //
|
||||||
|
.integer("_id", vaultId) //
|
||||||
|
.integer("FOLDER_CLOUD_ID", cloudId) //
|
||||||
|
.text("FOLDER_PATH", folderPath) //
|
||||||
|
.text("FOLDER_NAME", folderName) //
|
||||||
|
.text("CLOUD_TYPE", CloudType.WEBDAV.name) //
|
||||||
|
.text("PASSWORD", password) //
|
||||||
|
.integer("POSITION", position) //
|
||||||
|
.executeOn(db)
|
||||||
|
|
||||||
|
Upgrade4To5().applyTo(db, 4)
|
||||||
|
|
||||||
|
Sql.query("CLOUD_ENTITY").where("TYPE", Sql.eq(CloudType.WEBDAV.name)).executeOn(db).use {
|
||||||
|
it.moveToFirst()
|
||||||
|
Assert.assertThat(it.getInt(it.getColumnIndex("_id")), CoreMatchers.`is`(cloudId))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("ACCESS_TOKEN")), CoreMatchers.`is`(accessToken))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("URL")), CoreMatchers.`is`(cloudUrl))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("USERNAME")), CoreMatchers.`is`(username))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("WEBDAV_CERTIFICATE")), CoreMatchers.`is`(webdavCertificate))
|
||||||
|
}
|
||||||
|
|
||||||
|
Sql.query("VAULT_ENTITY").where("CLOUD_TYPE", Sql.eq(CloudType.WEBDAV.name)).executeOn(db).use {
|
||||||
|
it.moveToFirst()
|
||||||
|
Assert.assertThat(it.getInt(it.getColumnIndex("_id")), CoreMatchers.`is`(vaultId))
|
||||||
|
Assert.assertThat(it.getInt(it.getColumnIndex("FOLDER_CLOUD_ID")), CoreMatchers.`is`(cloudId))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_PATH")), CoreMatchers.`is`(folderPath))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_NAME")), CoreMatchers.`is`(folderName))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("CLOUD_TYPE")), CoreMatchers.`is`(CloudType.WEBDAV.name))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("PASSWORD")), CoreMatchers.`is`(password))
|
||||||
|
Assert.assertThat(it.getInt(it.getColumnIndex("POSITION")), CoreMatchers.`is`(position))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upgrade5To6() {
|
||||||
|
Upgrade0To1().applyTo(db, 0)
|
||||||
|
Upgrade1To2().applyTo(db, 1)
|
||||||
|
Upgrade2To3(context).applyTo(db, 2)
|
||||||
|
Upgrade3To4().applyTo(db, 3)
|
||||||
|
Upgrade4To5().applyTo(db, 4)
|
||||||
|
|
||||||
|
val cloudId = 15
|
||||||
|
val cloudUrl = "url"
|
||||||
|
val username = "username"
|
||||||
|
val webdavCertificate = "webdavCertificate"
|
||||||
|
val accessToken = "accessToken"
|
||||||
|
|
||||||
|
val vaultId = 25
|
||||||
|
val folderPath = "path"
|
||||||
|
val folderName = "name"
|
||||||
|
val password = "password"
|
||||||
|
val position = 10
|
||||||
|
|
||||||
|
Sql.insertInto("CLOUD_ENTITY") //
|
||||||
|
.integer("_id", cloudId) //
|
||||||
|
.text("TYPE", CloudType.WEBDAV.name) //
|
||||||
|
.text("ACCESS_TOKEN", accessToken) //
|
||||||
|
.text("URL", cloudUrl) //
|
||||||
|
.text("USERNAME", username) //
|
||||||
|
.text("WEBDAV_CERTIFICATE", webdavCertificate) //
|
||||||
|
.executeOn(db)
|
||||||
|
|
||||||
|
Sql.insertInto("VAULT_ENTITY") //
|
||||||
|
.integer("_id", vaultId) //
|
||||||
|
.integer("FOLDER_CLOUD_ID", cloudId) //
|
||||||
|
.text("FOLDER_PATH", folderPath) //
|
||||||
|
.text("FOLDER_NAME", folderName) //
|
||||||
|
.text("CLOUD_TYPE", CloudType.WEBDAV.name) //
|
||||||
|
.text("PASSWORD", password) //
|
||||||
|
.integer("POSITION", position) //
|
||||||
|
.executeOn(db)
|
||||||
|
|
||||||
|
Upgrade5To6().applyTo(db, 5)
|
||||||
|
|
||||||
|
Sql.query("CLOUD_ENTITY").where("TYPE", Sql.eq(CloudType.WEBDAV.name)).executeOn(db).use {
|
||||||
|
it.moveToFirst()
|
||||||
|
Assert.assertThat(it.getInt(it.getColumnIndex("_id")), CoreMatchers.`is`(cloudId))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("ACCESS_TOKEN")), CoreMatchers.`is`(accessToken))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("URL")), CoreMatchers.`is`(cloudUrl))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("USERNAME")), CoreMatchers.`is`(username))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("WEBDAV_CERTIFICATE")), CoreMatchers.`is`(webdavCertificate))
|
||||||
|
}
|
||||||
|
|
||||||
|
Sql.query("VAULT_ENTITY").where("CLOUD_TYPE", Sql.eq(CloudType.WEBDAV.name)).executeOn(db).use {
|
||||||
|
it.moveToFirst()
|
||||||
|
Assert.assertThat(it.getInt(it.getColumnIndex("_id")), CoreMatchers.`is`(vaultId))
|
||||||
|
Assert.assertThat(it.getInt(it.getColumnIndex("FOLDER_CLOUD_ID")), CoreMatchers.`is`(cloudId))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_PATH")), CoreMatchers.`is`(folderPath))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_NAME")), CoreMatchers.`is`(folderName))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("CLOUD_TYPE")), CoreMatchers.`is`(CloudType.WEBDAV.name))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("PASSWORD")), CoreMatchers.`is`(password))
|
||||||
|
Assert.assertThat(it.getInt(it.getColumnIndex("POSITION")), CoreMatchers.`is`(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upgrade6To7() {
|
||||||
|
Upgrade0To1().applyTo(db, 0)
|
||||||
|
Upgrade1To2().applyTo(db, 1)
|
||||||
|
Upgrade2To3(context).applyTo(db, 2)
|
||||||
|
Upgrade3To4().applyTo(db, 3)
|
||||||
|
Upgrade4To5().applyTo(db, 4)
|
||||||
|
Upgrade5To6().applyTo(db, 5)
|
||||||
|
|
||||||
|
val licenseToken = "licenseToken"
|
||||||
|
val releaseNote = "releaseNote"
|
||||||
|
val version = "version"
|
||||||
|
val urlApk = "urlApk"
|
||||||
|
val urlReleaseNote = "urlReleaseNote"
|
||||||
|
|
||||||
|
Sql.update("UPDATE_CHECK_ENTITY")
|
||||||
|
.set("LICENSE_TOKEN", Sql.toString(licenseToken))
|
||||||
|
.set("RELEASE_NOTE", Sql.toString(releaseNote))
|
||||||
|
.set("VERSION", Sql.toString(version))
|
||||||
|
.set("URL_TO_APK", Sql.toString(urlApk))
|
||||||
|
.set("URL_TO_RELEASE_NOTE", Sql.toString(urlReleaseNote))
|
||||||
|
.executeOn(db)
|
||||||
|
|
||||||
|
Upgrade6To7().applyTo(db, 6)
|
||||||
|
|
||||||
|
Sql.query("UPDATE_CHECK_ENTITY").executeOn(db).use {
|
||||||
|
it.moveToFirst()
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("LICENSE_TOKEN")), CoreMatchers.`is`(licenseToken))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("RELEASE_NOTE")), CoreMatchers.`is`(releaseNote))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("VERSION")), CoreMatchers.`is`(version))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_APK")), CoreMatchers.`is`(urlApk))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("APK_SHA256")), CoreMatchers.nullValue())
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_RELEASE_NOTE")), CoreMatchers.`is`(urlReleaseNote))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun recoverUpgrade6to7DueToSQLiteExceptionThrown() {
|
||||||
|
Upgrade0To1().applyTo(db, 0)
|
||||||
|
Upgrade1To2().applyTo(db, 1)
|
||||||
|
Upgrade2To3(context).applyTo(db, 2)
|
||||||
|
Upgrade3To4().applyTo(db, 3)
|
||||||
|
Upgrade4To5().applyTo(db, 4)
|
||||||
|
Upgrade5To6().applyTo(db, 5)
|
||||||
|
|
||||||
|
val licenseToken = "licenseToken"
|
||||||
|
|
||||||
|
Sql.update("UPDATE_CHECK_ENTITY")
|
||||||
|
.set("LICENSE_TOKEN", Sql.toString(licenseToken))
|
||||||
|
.set("RELEASE_NOTE", Sql.toString("releaseNote"))
|
||||||
|
.set("VERSION", Sql.toString("version"))
|
||||||
|
.set("URL_TO_APK", Sql.toString("urlApk"))
|
||||||
|
.set("URL_TO_RELEASE_NOTE", Sql.toString("urlReleaseNote"))
|
||||||
|
.executeOn(db)
|
||||||
|
|
||||||
|
Sql.alterTable("UPDATE_CHECK_ENTITY").renameTo("UPDATE_CHECK_ENTITY_OLD").executeOn(db)
|
||||||
|
|
||||||
|
Sql.createTable("UPDATE_CHECK_ENTITY") //
|
||||||
|
.id() //
|
||||||
|
.optionalText("LICENSE_TOKEN") //
|
||||||
|
.optionalText("RELEASE_NOTE") //
|
||||||
|
.optionalText("VERSION") //
|
||||||
|
.optionalText("URL_TO_APK") //
|
||||||
|
.optionalText("APK_SHA256") //
|
||||||
|
.optionalText("URL_TO_RELEASE_NOTE") //
|
||||||
|
.executeOn(db)
|
||||||
|
|
||||||
|
Upgrade6To7().tryToRecoverFromSQLiteException(db)
|
||||||
|
|
||||||
|
Sql.query("UPDATE_CHECK_ENTITY").executeOn(db).use {
|
||||||
|
it.moveToFirst()
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("LICENSE_TOKEN")), CoreMatchers.`is`(licenseToken))
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("RELEASE_NOTE")), CoreMatchers.nullValue())
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("VERSION")), CoreMatchers.nullValue())
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_APK")), CoreMatchers.nullValue())
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("APK_SHA256")), CoreMatchers.nullValue())
|
||||||
|
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_RELEASE_NOTE")), CoreMatchers.nullValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upgrade7To8() {
|
||||||
|
Upgrade0To1().applyTo(db, 0)
|
||||||
|
Upgrade1To2().applyTo(db, 1)
|
||||||
|
Upgrade2To3(context).applyTo(db, 2)
|
||||||
|
Upgrade3To4().applyTo(db, 3)
|
||||||
|
Upgrade4To5().applyTo(db, 4)
|
||||||
|
Upgrade5To6().applyTo(db, 5)
|
||||||
|
Upgrade6To7().applyTo(db, 6)
|
||||||
|
|
||||||
|
Sql.insertInto("CLOUD_ENTITY") //
|
||||||
|
.integer("_id", 15) //
|
||||||
|
.text("TYPE", CloudType.S3.name) //
|
||||||
|
.text("URL", "url") //
|
||||||
|
.text("USERNAME", "username") //
|
||||||
|
.text("WEBDAV_CERTIFICATE", "certificate") //
|
||||||
|
.text("ACCESS_TOKEN", "accessToken")
|
||||||
|
.text("S3_BUCKET", "s3Bucket") //
|
||||||
|
.text("S3_REGION", "s3Region") //
|
||||||
|
.text("S3_SECRET_KEY", "s3SecretKey") //
|
||||||
|
.executeOn(db)
|
||||||
|
|
||||||
|
Sql.insertInto("VAULT_ENTITY") //
|
||||||
|
.integer("_id", 25) //
|
||||||
|
.integer("FOLDER_CLOUD_ID", 15) //
|
||||||
|
.text("FOLDER_PATH", "path") //
|
||||||
|
.text("FOLDER_NAME", "name") //
|
||||||
|
.text("CLOUD_TYPE", CloudType.S3.name) //
|
||||||
|
.text("PASSWORD", "password") //
|
||||||
|
.integer("POSITION", 10) //
|
||||||
|
.executeOn(db)
|
||||||
|
|
||||||
|
Sql.query("CLOUD_ENTITY").executeOn(db).use {
|
||||||
|
Assert.assertThat(it.count, CoreMatchers.`is`(5))
|
||||||
|
}
|
||||||
|
|
||||||
|
Upgrade7To8().applyTo(db, 7)
|
||||||
|
|
||||||
|
Sql.query("CLOUD_ENTITY").executeOn(db).use {
|
||||||
|
Assert.assertThat(it.count, CoreMatchers.`is`(4))
|
||||||
|
}
|
||||||
|
|
||||||
|
Sql.query("VAULT_ENTITY").executeOn(db).use {
|
||||||
|
Assert.assertThat(it.moveToFirst(), CoreMatchers.`is`(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upgrade8To9() {
|
||||||
|
Upgrade0To1().applyTo(db, 0)
|
||||||
|
Upgrade1To2().applyTo(db, 1)
|
||||||
|
Upgrade2To3(context).applyTo(db, 2)
|
||||||
|
Upgrade3To4().applyTo(db, 3)
|
||||||
|
Upgrade4To5().applyTo(db, 4)
|
||||||
|
Upgrade5To6().applyTo(db, 5)
|
||||||
|
Upgrade6To7().applyTo(db, 6)
|
||||||
|
Upgrade7To8().applyTo(db, 7)
|
||||||
|
|
||||||
|
sharedPreferencesHandler.setBetaScreenDialogAlreadyShown(true)
|
||||||
|
|
||||||
|
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
|
||||||
|
|
||||||
|
Assert.assertThat(sharedPreferencesHandler.isBetaModeAlreadyShown(), CoreMatchers.`is`(false))
|
||||||
|
}
|
||||||
|
}
|
@ -1,223 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DownloadState;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public abstract class InterceptingCloudContentRepository<CloudType extends Cloud, NodeType extends CloudNode, DirType extends CloudFolder, FileType extends CloudFile> implements CloudContentRepository<CloudType, NodeType, DirType, FileType> {
|
|
||||||
|
|
||||||
private final CloudContentRepository<CloudType, NodeType, DirType, FileType> delegate;
|
|
||||||
|
|
||||||
protected InterceptingCloudContentRepository(CloudContentRepository<CloudType, NodeType, DirType, FileType> delegate) {
|
|
||||||
this.delegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void throwWrappedIfRequired(Exception e) throws BackendException;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DirType root(CloudType cloud) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.root(cloud);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DirType resolve(CloudType cloud, String path) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.resolve(cloud, path);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileType file(DirType parent, String name) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.file(parent, name);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileType file(DirType parent, String name, Optional<Long> size) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.file(parent, name, size);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DirType folder(DirType parent, String name) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.folder(parent, name);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean exists(NodeType node) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.exists(node);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<? extends CloudNode> list(DirType folder) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.list(folder);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DirType create(DirType folder) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.create(folder);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DirType move(DirType source, DirType target) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.move(source, target);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileType move(FileType source, FileType target) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.move(source, target);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileType write(FileType file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.write(file, data, progressAware, replace, size);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void read(FileType file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
|
||||||
try {
|
|
||||||
delegate.read(file, encryptedTmpFile, data, progressAware);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(NodeType node) throws BackendException {
|
|
||||||
try {
|
|
||||||
delegate.delete(node);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String checkAuthenticationAndRetrieveCurrentAccount(CloudType cloud) throws BackendException {
|
|
||||||
try {
|
|
||||||
return delegate.checkAuthenticationAndRetrieveCurrentAccount(cloud);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void logout(CloudType cloud) throws BackendException {
|
|
||||||
try {
|
|
||||||
delegate.logout(cloud);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,216 @@
|
|||||||
|
package org.cryptomator.data.cloud
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DownloadState
|
||||||
|
import org.cryptomator.domain.usecases.cloud.UploadState
|
||||||
|
import java.io.File
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
abstract class InterceptingCloudContentRepository<CloudType : Cloud, NodeType : CloudNode, DirType : CloudFolder, FileType : CloudFile> protected constructor(private val delegate: CloudContentRepository<CloudType, NodeType, DirType, FileType>) :
|
||||||
|
CloudContentRepository<CloudType, NodeType, DirType, FileType> {
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
protected abstract fun throwWrappedIfRequired(e: Exception)
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun root(cloud: CloudType): DirType {
|
||||||
|
return try {
|
||||||
|
delegate.root(cloud)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun resolve(cloud: CloudType, path: String): DirType {
|
||||||
|
return try {
|
||||||
|
delegate.resolve(cloud, path)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun file(parent: DirType, name: String): FileType {
|
||||||
|
return try {
|
||||||
|
delegate.file(parent, name)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun file(parent: DirType, name: String, size: Long?): FileType {
|
||||||
|
return try {
|
||||||
|
delegate.file(parent, name, size)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun folder(parent: DirType, name: String): DirType {
|
||||||
|
return try {
|
||||||
|
delegate.folder(parent, name)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun exists(node: NodeType): Boolean {
|
||||||
|
return try {
|
||||||
|
delegate.exists(node)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun list(folder: DirType): List<NodeType> {
|
||||||
|
return try {
|
||||||
|
delegate.list(folder)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun create(folder: DirType): DirType {
|
||||||
|
return try {
|
||||||
|
delegate.create(folder)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: DirType, target: DirType): DirType {
|
||||||
|
return try {
|
||||||
|
delegate.move(source, target)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: FileType, target: FileType): FileType {
|
||||||
|
return try {
|
||||||
|
delegate.move(source, target)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun write(file: FileType, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): FileType {
|
||||||
|
return try {
|
||||||
|
delegate.write(file, data, progressAware, replace, size)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun read(file: FileType, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
|
||||||
|
try {
|
||||||
|
delegate.read(file, encryptedTmpFile, data, progressAware)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun delete(node: NodeType) {
|
||||||
|
try {
|
||||||
|
delegate.delete(node)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: CloudType): String {
|
||||||
|
return try {
|
||||||
|
delegate.checkAuthenticationAndRetrieveCurrentAccount(cloud)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun logout(cloud: CloudType) {
|
||||||
|
try {
|
||||||
|
delegate.logout(cloud)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
throwWrappedIfRequired(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding;
|
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for generating a suffix for the backup file to make it unique to its original master key file.
|
|
||||||
*/
|
|
||||||
class BackupFileIdSuffixGenerator {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the SHA-256 digest of the given byte array and returns a file suffix containing the first 4 bytes in hex string format.
|
|
||||||
*
|
|
||||||
* @param fileBytes the input byte for which the digest is computed
|
|
||||||
* @return "." + first 4 bytes of SHA-256 digest in hex string format
|
|
||||||
*/
|
|
||||||
static String generate(byte[] fileBytes) {
|
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
||||||
byte[] digest = md.digest(fileBytes);
|
|
||||||
return "." + BaseEncoding.base16().encode(digest, 0, 4);
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new IllegalStateException("Every Java Platform must support the Message Digest algorithm SHA-256", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,28 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import com.google.common.io.BaseEncoding
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for generating a suffix for the backup file to make it unique to its original master key file.
|
||||||
|
*/
|
||||||
|
internal object BackupFileIdSuffixGenerator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the SHA-256 digest of the given byte array and returns a file suffix containing the first 4 bytes in hex string format.
|
||||||
|
*
|
||||||
|
* @param fileBytes the input byte for which the digest is computed
|
||||||
|
* @return "." + first 4 bytes of SHA-256 digest in hex string format
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun generate(fileBytes: ByteArray): String {
|
||||||
|
return try {
|
||||||
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
|
val digest = md.digest(fileBytes)
|
||||||
|
"." + BaseEncoding.base16().encode(digest, 0, 4)
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw IllegalStateException("Every Java Platform must support the Message Digest algorithm SHA-256", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,135 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.cryptomator.cryptolib.api.Cryptor;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
|
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DownloadState;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
import org.cryptomator.util.Supplier;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static java.lang.String.format;
|
|
||||||
|
|
||||||
class CryptoCloudContentRepository implements CloudContentRepository<CryptoCloud, CryptoNode, CryptoFolder, CryptoFile> {
|
|
||||||
|
|
||||||
private final CryptoImplDecorator cryptoImpl;
|
|
||||||
|
|
||||||
CryptoCloudContentRepository(Context context, CloudContentRepository cloudContentRepository, CryptoCloud cloud, Supplier<Cryptor> cryptor) {
|
|
||||||
CloudFolder vaultLocation;
|
|
||||||
try {
|
|
||||||
vaultLocation = cloudContentRepository.resolve(cloud.getVault().getCloud(), cloud.getVault().getPath());
|
|
||||||
} catch (BackendException e) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (cloud.getVault().getVersion()) {
|
|
||||||
case 7:
|
|
||||||
this.cryptoImpl = new CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7());
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
case 5:
|
|
||||||
this.cryptoImpl = new CryptoImplVaultFormatPre7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormatPre7());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException(format("No CryptoImpl for vault version %d.", cloud.getVault().getVersion()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized CryptoFolder root(CryptoCloud cloud) throws BackendException {
|
|
||||||
return cryptoImpl.root(cloud);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFolder resolve(CryptoCloud cloud, String path) throws BackendException {
|
|
||||||
return cryptoImpl.resolve(cloud, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFile file(CryptoFolder parent, String name) throws BackendException {
|
|
||||||
return cryptoImpl.file(parent, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFile file(CryptoFolder parent, String name, Optional<Long> size) throws BackendException {
|
|
||||||
return cryptoImpl.file(parent, name, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFolder folder(CryptoFolder parent, String name) throws BackendException {
|
|
||||||
return cryptoImpl.folder(parent, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean exists(CryptoNode node) throws BackendException {
|
|
||||||
return cryptoImpl.exists(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CryptoNode> list(CryptoFolder folder) throws BackendException {
|
|
||||||
return cryptoImpl.list(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFolder create(CryptoFolder folder) throws BackendException {
|
|
||||||
try {
|
|
||||||
return cryptoImpl.create(folder);
|
|
||||||
} catch (CloudNodeAlreadyExistsException e) {
|
|
||||||
throw new CloudNodeAlreadyExistsException(folder.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException {
|
|
||||||
try {
|
|
||||||
return cryptoImpl.move(source, target);
|
|
||||||
} catch (CloudNodeAlreadyExistsException e) {
|
|
||||||
throw new CloudNodeAlreadyExistsException(target.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException {
|
|
||||||
try {
|
|
||||||
return cryptoImpl.move(source, target);
|
|
||||||
} catch (CloudNodeAlreadyExistsException e) {
|
|
||||||
throw new CloudNodeAlreadyExistsException(target.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFile write(CryptoFile file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException {
|
|
||||||
return cryptoImpl.write(file, data, progressAware, replace, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void read(CryptoFile file, Optional<File> tmpEncryptedFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
|
||||||
cryptoImpl.read(file, data, progressAware);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(CryptoNode node) throws BackendException {
|
|
||||||
cryptoImpl.delete(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String checkAuthenticationAndRetrieveCurrentAccount(CryptoCloud cloud) throws BackendException {
|
|
||||||
return cryptoImpl.currentAccount(cloud);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void logout(CryptoCloud cloud) throws BackendException {
|
|
||||||
// empty
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,128 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.cryptomator.cryptolib.api.Cryptor
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
|
||||||
|
import org.cryptomator.domain.exception.FatalBackendException
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DownloadState
|
||||||
|
import org.cryptomator.domain.usecases.cloud.UploadState
|
||||||
|
import java.io.File
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
|
internal class CryptoCloudContentRepository(context: Context, cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>, cloud: CryptoCloud, cryptor: Supplier<Cryptor>) :
|
||||||
|
CloudContentRepository<CryptoCloud, CryptoNode, CryptoFolder, CryptoFile> {
|
||||||
|
|
||||||
|
private var cryptoImpl: CryptoImplDecorator
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun root(cloud: CryptoCloud): CryptoFolder {
|
||||||
|
return cryptoImpl.root(cloud)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(cloud: CryptoCloud, path: String): CryptoFolder {
|
||||||
|
return cryptoImpl.resolve(cloud, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun file(parent: CryptoFolder, name: String): CryptoFile {
|
||||||
|
return cryptoImpl.file(parent, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun file(parent: CryptoFolder, name: String, size: Long?): CryptoFile {
|
||||||
|
return cryptoImpl.file(parent, name, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun folder(parent: CryptoFolder, name: String): CryptoFolder {
|
||||||
|
return cryptoImpl.folder(parent, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun exists(node: CryptoNode): Boolean {
|
||||||
|
return cryptoImpl.exists(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun list(folder: CryptoFolder): List<CryptoNode> {
|
||||||
|
return cryptoImpl.list(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun create(folder: CryptoFolder): CryptoFolder {
|
||||||
|
return try {
|
||||||
|
cryptoImpl.create(folder)
|
||||||
|
} catch (e: CloudNodeAlreadyExistsException) {
|
||||||
|
throw CloudNodeAlreadyExistsException(folder.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder {
|
||||||
|
return try {
|
||||||
|
cryptoImpl.move(source, target)
|
||||||
|
} catch (e: CloudNodeAlreadyExistsException) {
|
||||||
|
throw CloudNodeAlreadyExistsException(target.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: CryptoFile, target: CryptoFile): CryptoFile {
|
||||||
|
return try {
|
||||||
|
cryptoImpl.move(source, target)
|
||||||
|
} catch (e: CloudNodeAlreadyExistsException) {
|
||||||
|
throw CloudNodeAlreadyExistsException(target.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun write(file: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): CryptoFile {
|
||||||
|
return cryptoImpl.write(file, data, progressAware, replace, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun read(file: CryptoFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
|
||||||
|
cryptoImpl.read(file, data, progressAware)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun delete(node: CryptoNode) {
|
||||||
|
cryptoImpl.delete(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: CryptoCloud): String {
|
||||||
|
return cryptoImpl.currentAccount(cloud)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun logout(cloud: CryptoCloud) {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val vaultLocation: CloudFolder = try {
|
||||||
|
cloudContentRepository.resolve(cloud.vault.cloud, cloud.vault.path)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoImpl = when (cloud.vault.format) {
|
||||||
|
7 -> CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormat7())
|
||||||
|
8 -> CryptoImplVaultFormat8(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormat7(), cloud.vault.shorteningThreshold)
|
||||||
|
6, 5 -> CryptoImplVaultFormatPre7(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormatPre7())
|
||||||
|
else -> throw IllegalStateException(String.format("No CryptoImpl for vault format %d.", cloud.vault.format))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,13 +2,14 @@ package org.cryptomator.data.cloud.crypto;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
|
||||||
import org.cryptomator.cryptolib.api.Cryptor;
|
import org.cryptomator.cryptolib.api.Cryptor;
|
||||||
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
|
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
|
||||||
import org.cryptomator.domain.Cloud;
|
import org.cryptomator.domain.Cloud;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.MissingCryptorException;
|
import org.cryptomator.domain.exception.MissingCryptorException;
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@ -38,7 +39,7 @@ public class CryptoCloudContentRepositoryFactory implements CloudContentReposito
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
|
public CloudContentRepository<CryptoCloud, CryptoNode, CryptoFolder, CryptoFile> cloudContentRepositoryFor(Cloud cloud) {
|
||||||
CryptoCloud cryptoCloud = (CryptoCloud) cloud;
|
CryptoCloud cryptoCloud = (CryptoCloud) cloud;
|
||||||
Vault vault = cryptoCloud.getVault();
|
Vault vault = cryptoCloud.getVault();
|
||||||
return new CryptoCloudContentRepository(context, cloudContentRepository.get(), cryptoCloud, cryptors.get(vault));
|
return new CryptoCloudContentRepository(context, cloudContentRepository.get(), cryptoCloud, cryptors.get(vault));
|
||||||
@ -50,7 +51,7 @@ public class CryptoCloudContentRepositoryFactory implements CloudContentReposito
|
|||||||
|
|
||||||
public void deregisterCryptor(Vault vault, boolean assertPresent) {
|
public void deregisterCryptor(Vault vault, boolean assertPresent) {
|
||||||
Optional<Cryptor> cryptor = cryptors.remove(vault);
|
Optional<Cryptor> cryptor = cryptors.remove(vault);
|
||||||
if (cryptor.isAbsent()) {
|
if (!cryptor.isPresent()) {
|
||||||
if (assertPresent) {
|
if (assertPresent) {
|
||||||
throw new IllegalStateException(format("No cryptor registered for vault %s", vault));
|
throw new IllegalStateException(format("No cryptor registered for vault %s", vault));
|
||||||
}
|
}
|
||||||
|
@ -1,223 +1,97 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
package org.cryptomator.data.cloud.crypto;
|
||||||
|
|
||||||
import org.cryptomator.cryptolib.Cryptors;
|
import com.google.common.base.Optional;
|
||||||
import org.cryptomator.cryptolib.api.Cryptor;
|
|
||||||
import org.cryptomator.cryptolib.api.CryptorProvider;
|
|
||||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
|
||||||
import org.cryptomator.cryptolib.api.KeyFile;
|
|
||||||
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
|
|
||||||
import org.cryptomator.domain.Cloud;
|
import org.cryptomator.domain.Cloud;
|
||||||
import org.cryptomator.domain.CloudFile;
|
import org.cryptomator.domain.CloudFile;
|
||||||
import org.cryptomator.domain.CloudFolder;
|
import org.cryptomator.domain.CloudFolder;
|
||||||
|
import org.cryptomator.domain.CloudNode;
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.exception.CancellationException;
|
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||||
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource;
|
import org.cryptomator.domain.usecases.ProgressAware;
|
||||||
import org.cryptomator.domain.usecases.cloud.Flag;
|
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||||
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.text.Normalizer;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import static android.R.attr.version;
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME;
|
||||||
import static java.text.Normalizer.normalize;
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME;
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_BACKUP_FILE_EXT;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_FILE_NAME;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MIN_VAULT_VERSION;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.ROOT_DIR_ID;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VERSION_WITH_NORMALIZED_PASSWORDS;
|
|
||||||
import static org.cryptomator.domain.Vault.aCopyOf;
|
import static org.cryptomator.domain.Vault.aCopyOf;
|
||||||
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class CryptoCloudFactory {
|
public class CryptoCloudFactory {
|
||||||
|
|
||||||
private final CryptorProvider cryptorProvider;
|
private final CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile> cloudContentRepository;
|
||||||
private final CloudContentRepository cloudContentRepository;
|
|
||||||
private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
|
private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
|
||||||
|
private final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public CryptoCloudFactory( //
|
public CryptoCloudFactory(CloudContentRepository/*<Cloud, CloudNode, CloudFolder, CloudFile>*/ cloudContentRepository, //
|
||||||
CloudContentRepository cloudContentRepository, //
|
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) {
|
||||||
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory, //
|
|
||||||
CryptorProvider cryptorProvider) {
|
|
||||||
this.cryptorProvider = cryptorProvider;
|
|
||||||
this.cloudContentRepository = cloudContentRepository;
|
this.cloudContentRepository = cloudContentRepository;
|
||||||
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
|
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void create(CloudFolder location, CharSequence password) throws BackendException {
|
public void create(CloudFolder location, CharSequence password) throws BackendException {
|
||||||
Cryptor cryptor = cryptorProvider.createNew();
|
cryptoCloudProvider(Optional.absent()).create(location, password);
|
||||||
try {
|
|
||||||
KeyFile keyFile = cryptor.writeKeysToMasterkeyFile(normalizePassword(password, version), MAX_VAULT_VERSION);
|
|
||||||
writeKeyFile(location, keyFile);
|
|
||||||
createRootFolder(location, cryptor);
|
|
||||||
} finally {
|
|
||||||
cryptor.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createRootFolder(CloudFolder location, Cryptor cryptor) throws BackendException {
|
|
||||||
CloudFolder dFolder = cloudContentRepository.folder(location, DATA_DIR_NAME);
|
|
||||||
dFolder = cloudContentRepository.create(dFolder);
|
|
||||||
String rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(ROOT_DIR_ID);
|
|
||||||
CloudFolder lvl1Folder = cloudContentRepository.folder(dFolder, rootDirHash.substring(0, 2));
|
|
||||||
lvl1Folder = cloudContentRepository.create(lvl1Folder);
|
|
||||||
CloudFolder lvl2Folder = cloudContentRepository.folder(lvl1Folder, rootDirHash.substring(2));
|
|
||||||
cloudContentRepository.create(lvl2Folder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cloud decryptedViewOf(Vault vault) throws BackendException {
|
public Cloud decryptedViewOf(Vault vault) throws BackendException {
|
||||||
return new CryptoCloud(aCopyOf(vault).build());
|
return new CryptoCloud(aCopyOf(vault).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vault unlock(Vault vault, CharSequence password, Flag cancelledFlag) throws BackendException {
|
public Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException {
|
||||||
return unlock(createUnlockToken(vault), password, cancelledFlag);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vault unlock(UnlockToken token, CharSequence password, Flag cancelledFlag) throws BackendException {
|
|
||||||
UnlockTokenImpl impl = (UnlockTokenImpl) token;
|
|
||||||
Cryptor cryptor = cryptorFor(impl.getKeyFile(), password);
|
|
||||||
|
|
||||||
if (cancelledFlag.get()) {
|
|
||||||
throw new CancellationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
cryptoCloudContentRepositoryFactory.registerCryptor(impl.getVault(), cryptor);
|
|
||||||
|
|
||||||
return aCopyOf(token.getVault()) //
|
|
||||||
.withVersion(impl.getKeyFile().getVersion()) //
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public UnlockTokenImpl createUnlockToken(Vault vault) throws BackendException {
|
|
||||||
CloudFolder vaultLocation = cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
CloudFolder vaultLocation = cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
||||||
return createUnlockToken(vault, vaultLocation);
|
String jwt = new String(readConfigFileData(vaultLocation), StandardCharsets.UTF_8);
|
||||||
|
return Optional.of(VaultConfig.decode(jwt));
|
||||||
}
|
}
|
||||||
|
|
||||||
private UnlockTokenImpl createUnlockToken(Vault vault, CloudFolder location) throws BackendException {
|
private byte[] readConfigFileData(CloudFolder location) throws BackendException {
|
||||||
byte[] keyFileData = readKeyFileData(location);
|
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||||
UnlockTokenImpl unlockToken = new UnlockTokenImpl(vault, keyFileData);
|
CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME);
|
||||||
assertVaultVersionIsSupported(unlockToken.getKeyFile().getVersion());
|
cloudContentRepository.read(vaultFile, null, data, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD);
|
||||||
return unlockToken;
|
return data.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cryptor cryptorFor(KeyFile keyFile, CharSequence password) {
|
public Vault unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||||
return cryptorProvider.createFromKeyFile(keyFile, normalizePassword(password, keyFile.getVersion()), keyFile.getVersion());
|
return cryptoCloudProvider(unverifiedVaultConfig).unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CloudFolder vaultLocation(Vault vault) throws BackendException {
|
public Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||||
return cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
return cryptoCloudProvider(unverifiedVaultConfig).unlock(token, unverifiedVaultConfig, password, cancelledFlag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException {
|
public UnlockToken createUnlockToken(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException {
|
||||||
try {
|
return cryptoCloudProvider(unverifiedVaultConfig).createUnlockToken(vault, unverifiedVaultConfig);
|
||||||
// create a cryptor, which checks the password, then destroy it immediately
|
}
|
||||||
cryptorFor(createUnlockToken(vault).getKeyFile(), password).destroy();
|
|
||||||
return true;
|
public boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException {
|
||||||
} catch (InvalidPassphraseException e) {
|
return cryptoCloudProvider(unverifiedVaultConfig).isVaultPasswordValid(vault, unverifiedVaultConfig, password);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void lock(Vault vault) {
|
public void lock(Vault vault) {
|
||||||
cryptoCloudContentRepositoryFactory.deregisterCryptor(vault);
|
cryptoCloudContentRepositoryFactory.deregisterCryptor(vault);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertVaultVersionIsSupported(int version) {
|
public void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException {
|
||||||
if (version < MIN_VAULT_VERSION) {
|
cryptoCloudProvider(unverifiedVaultConfig).changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword);
|
||||||
throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION);
|
|
||||||
} else if (version > MAX_VAULT_VERSION) {
|
|
||||||
throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeKeyFile(CloudFolder location, KeyFile keyFile) throws BackendException {
|
private CryptoCloudProvider cryptoCloudProvider(Optional<UnverifiedVaultConfig> unverifiedVaultConfigOptional) {
|
||||||
byte[] data = keyFile.serialize();
|
if (unverifiedVaultConfigOptional.isPresent()) {
|
||||||
cloudContentRepository.write(masterkeyFile(location), ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, false, data.length);
|
if (MASTERKEY_SCHEME.equals(unverifiedVaultConfigOptional.get().getKeyId().getScheme())) {
|
||||||
}
|
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
|
||||||
|
}
|
||||||
private byte[] readKeyFileData(CloudFolder location) throws BackendException {
|
throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme()));
|
||||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
|
||||||
cloudContentRepository.read(masterkeyFile(location), Optional.empty(), data, NO_OP_PROGRESS_AWARE);
|
|
||||||
return data.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFile masterkeyFile(CloudFolder location) throws BackendException {
|
|
||||||
return cloudContentRepository.file(location, MASTERKEY_FILE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFile masterkeyBackupFile(CloudFolder location, byte[] data) throws BackendException {
|
|
||||||
String fileName = MASTERKEY_FILE_NAME + BackupFileIdSuffixGenerator.generate(data) + MASTERKEY_BACKUP_FILE_EXT;
|
|
||||||
return cloudContentRepository.file(location, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException {
|
|
||||||
CloudFolder vaultLocation = vaultLocation(vault);
|
|
||||||
ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream();
|
|
||||||
cloudContentRepository.read(masterkeyFile(vaultLocation), Optional.empty(), dataOutputStream, NO_OP_PROGRESS_AWARE);
|
|
||||||
|
|
||||||
byte[] data = dataOutputStream.toByteArray();
|
|
||||||
int vaultVersion = KeyFile.parse(data).getVersion();
|
|
||||||
|
|
||||||
createBackupMasterKeyFile(data, vaultLocation);
|
|
||||||
createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, vaultLocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createBackupMasterKeyFile(byte[] data, CloudFolder vaultLocation) throws BackendException {
|
|
||||||
cloudContentRepository.write( //
|
|
||||||
masterkeyBackupFile(vaultLocation, data), //
|
|
||||||
ByteArrayDataSource.from(data), //
|
|
||||||
NO_OP_PROGRESS_AWARE, //
|
|
||||||
true, //
|
|
||||||
data.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createNewMasterKeyFile(byte[] data, int vaultVersion, String oldPassword, String newPassword, CloudFolder vaultLocation) throws BackendException {
|
|
||||||
byte[] newMasterKeyFile = Cryptors.changePassphrase(cryptorProvider, //
|
|
||||||
data, //
|
|
||||||
normalizePassword(oldPassword, vaultVersion), //
|
|
||||||
normalizePassword(newPassword, vaultVersion));
|
|
||||||
cloudContentRepository.write(masterkeyFile(vaultLocation), //
|
|
||||||
ByteArrayDataSource.from(newMasterKeyFile), //
|
|
||||||
NO_OP_PROGRESS_AWARE, //
|
|
||||||
true, //
|
|
||||||
newMasterKeyFile.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CharSequence normalizePassword(CharSequence password, int vaultVersion) {
|
|
||||||
if (vaultVersion >= VERSION_WITH_NORMALIZED_PASSWORDS) {
|
|
||||||
return normalize(password, Normalizer.Form.NFC);
|
|
||||||
} else {
|
} else {
|
||||||
return password;
|
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class UnlockTokenImpl implements UnlockToken {
|
|
||||||
|
|
||||||
private final Vault vault;
|
|
||||||
private final byte[] keyFileData;
|
|
||||||
|
|
||||||
private UnlockTokenImpl(Vault vault, byte[] keyFileData) {
|
|
||||||
this.vault = vault;
|
|
||||||
this.keyFileData = keyFileData;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Vault getVault() {
|
|
||||||
return vault;
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeyFile getKeyFile() {
|
|
||||||
return KeyFile.parse(keyFileData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto;
|
||||||
|
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
|
||||||
|
import org.cryptomator.domain.CloudFolder;
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
|
import org.cryptomator.domain.Vault;
|
||||||
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
|
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||||
|
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||||
|
|
||||||
|
public interface CryptoCloudProvider {
|
||||||
|
|
||||||
|
void create(CloudFolder location, CharSequence password) throws BackendException;
|
||||||
|
|
||||||
|
Vault unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException;
|
||||||
|
|
||||||
|
UnlockToken createUnlockToken(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException;
|
||||||
|
|
||||||
|
Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException;
|
||||||
|
|
||||||
|
boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException;
|
||||||
|
|
||||||
|
void lock(Vault vault);
|
||||||
|
|
||||||
|
void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException;
|
||||||
|
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
class CryptoConstants {
|
|
||||||
|
|
||||||
static final String ROOT_DIR_ID = "";
|
|
||||||
static final String DATA_DIR_NAME = "d";
|
|
||||||
static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator";
|
|
||||||
static final String MASTERKEY_BACKUP_FILE_EXT = ".bkup";
|
|
||||||
|
|
||||||
static final int MAX_VAULT_VERSION = 7;
|
|
||||||
static final int VERSION_WITH_NORMALIZED_PASSWORDS = 6;
|
|
||||||
static final int MIN_VAULT_VERSION = 5;
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,22 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import org.cryptomator.cryptolib.api.CryptorProvider
|
||||||
|
|
||||||
|
object CryptoConstants {
|
||||||
|
|
||||||
|
const val MASTERKEY_SCHEME = "masterkeyfile"
|
||||||
|
const val MASTERKEY_FILE_NAME = "masterkey.cryptomator"
|
||||||
|
const val ROOT_DIR_ID = ""
|
||||||
|
const val DATA_DIR_NAME = "d"
|
||||||
|
const val VAULT_FILE_NAME = "vault.cryptomator"
|
||||||
|
const val MASTERKEY_BACKUP_FILE_EXT = ".bkup"
|
||||||
|
const val DEFAULT_MASTERKEY_FILE_VERSION = 999
|
||||||
|
const val MAX_VAULT_VERSION = 8
|
||||||
|
const val MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG = 7
|
||||||
|
const val VERSION_WITH_NORMALIZED_PASSWORDS = 6
|
||||||
|
const val MIN_VAULT_VERSION = 5
|
||||||
|
const val DEFAULT_MAX_FILE_NAME = 220
|
||||||
|
val PEPPER = ByteArray(0)
|
||||||
|
val DEFAULT_CIPHER_COMBO = CryptorProvider.Scheme.SIV_CTRMAC
|
||||||
|
|
||||||
|
}
|
@ -1,82 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
class CryptoFile implements CloudFile, CryptoNode {
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
private final String path;
|
|
||||||
private final Optional<Long> size;
|
|
||||||
private final CloudFile cloudFile;
|
|
||||||
private final CryptoFolder parent;
|
|
||||||
|
|
||||||
public CryptoFile(CryptoFolder parent, String name, String path, Optional<Long> size, CloudFile cloudFile) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
this.size = size;
|
|
||||||
this.cloudFile = cloudFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return parent.getCloud();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFolder getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Long> getSize() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Date> getModified() {
|
|
||||||
return cloudFile.getModified();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The actual file in the underlying, i.e. decorated, CloudContentRepository
|
|
||||||
*/
|
|
||||||
CloudFile getCloudFile() {
|
|
||||||
return cloudFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (obj == null || getClass() != obj.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (obj == this) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return internalEquals((CryptoFile) obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean internalEquals(CryptoFile obj) {
|
|
||||||
return path != null && path.equals(obj.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return path == null ? 0 : path.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,37 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class CryptoFile(
|
||||||
|
override val parent: CryptoFolder, override val name: String, override val path: String, override val size: Long?,
|
||||||
|
/**
|
||||||
|
* @return The actual file in the underlying, i.e. decorated, CloudContentRepository
|
||||||
|
*/
|
||||||
|
val cloudFile: CloudFile
|
||||||
|
) : CloudFile, CryptoNode {
|
||||||
|
|
||||||
|
override val cloud: Cloud?
|
||||||
|
get() = parent.cloud
|
||||||
|
|
||||||
|
override val modified: Date?
|
||||||
|
get() = cloudFile.modified
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other == null || javaClass != other.javaClass) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return if (other === this) {
|
||||||
|
true
|
||||||
|
} else internalEquals(other as CryptoFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun internalEquals(obj: CryptoFile): Boolean {
|
||||||
|
return path == obj.path
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return path.hashCode()
|
||||||
|
}
|
||||||
|
}
|
@ -1,72 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
|
|
||||||
class CryptoFolder implements CloudFolder, CryptoNode {
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
private final String path;
|
|
||||||
private final CryptoFolder parent;
|
|
||||||
private final CloudFile dirFile;
|
|
||||||
|
|
||||||
CryptoFolder(CryptoFolder parent, String name, String path, CloudFile dirFile) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
this.dirFile = dirFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return parent.getCloud();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFolder getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the file containing the directory id, in the underlying, i.e. decorated, CloudContentRepository
|
|
||||||
*/
|
|
||||||
CloudFile getDirFile() {
|
|
||||||
return dirFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (obj == null || getClass() != obj.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (obj == this) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return internalEquals((CryptoFolder) obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean internalEquals(CryptoFolder obj) {
|
|
||||||
return path != null && path.equals(obj.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return path == null ? 0 : path.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFolder withCloud(Cloud cloud) {
|
|
||||||
return new CryptoFolder(parent.withCloud(cloud), name, path, dirFile);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,38 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
|
||||||
|
open class CryptoFolder(
|
||||||
|
override val parent: CryptoFolder?, override val name: String, override val path: String,
|
||||||
|
/**
|
||||||
|
* @return the file containing the directory id, in the underlying, i.e. decorated, CloudContentRepository
|
||||||
|
*/
|
||||||
|
val dirFile: CloudFile?
|
||||||
|
) : CloudFolder, CryptoNode {
|
||||||
|
|
||||||
|
override val cloud: Cloud?
|
||||||
|
get() = parent?.cloud
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other == null || javaClass != other.javaClass) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return if (other === this) {
|
||||||
|
true
|
||||||
|
} else internalEquals(other as CryptoFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun internalEquals(obj: CryptoFolder): Boolean {
|
||||||
|
return path == obj.path
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return path.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withCloud(cloud: Cloud?): CryptoFolder? {
|
||||||
|
return CryptoFolder(parent?.withCloud(cloud), name, path, dirFile)
|
||||||
|
}
|
||||||
|
}
|
@ -1,423 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.cryptomator.cryptolib.Cryptors;
|
|
||||||
import org.cryptomator.cryptolib.DecryptingReadableByteChannel;
|
|
||||||
import org.cryptomator.cryptolib.EncryptingWritableByteChannel;
|
|
||||||
import org.cryptomator.cryptolib.api.Cryptor;
|
|
||||||
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo;
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
|
|
||||||
import org.cryptomator.domain.exception.EmptyDirFileException;
|
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
|
||||||
import org.cryptomator.domain.exception.NoDirFileException;
|
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
|
||||||
import org.cryptomator.domain.usecases.DownloadFileReplacingProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DownloadState;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.FileBasedDataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.Progress;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
import org.cryptomator.util.Supplier;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.channels.Channels;
|
|
||||||
import java.nio.channels.ReadableByteChannel;
|
|
||||||
import java.nio.channels.WritableByteChannel;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME;
|
|
||||||
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
|
||||||
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
|
|
||||||
|
|
||||||
abstract class CryptoImplDecorator {
|
|
||||||
|
|
||||||
final CloudContentRepository cloudContentRepository;
|
|
||||||
final Context context;
|
|
||||||
final DirIdCache dirIdCache;
|
|
||||||
|
|
||||||
private final Supplier<Cryptor> cryptor;
|
|
||||||
private final CloudFolder storageLocation;
|
|
||||||
|
|
||||||
private RootCryptoFolder root;
|
|
||||||
|
|
||||||
CryptoImplDecorator(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
|
|
||||||
this.context = context;
|
|
||||||
this.cryptor = cryptor;
|
|
||||||
this.cloudContentRepository = cloudContentRepository;
|
|
||||||
this.storageLocation = storageLocation;
|
|
||||||
this.dirIdCache = dirIdCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException;
|
|
||||||
|
|
||||||
abstract String decryptName(String dirId, String encryptedName);
|
|
||||||
|
|
||||||
abstract String encryptName(CryptoFolder cryptoParent, String name) throws BackendException;
|
|
||||||
|
|
||||||
abstract Optional<String> extractEncryptedName(String ciphertextName);
|
|
||||||
|
|
||||||
abstract List<CryptoNode> list(CryptoFolder cryptoFolder) throws BackendException;
|
|
||||||
|
|
||||||
abstract String encryptFolderName(CryptoFolder cryptoFolder, String name) throws BackendException;
|
|
||||||
|
|
||||||
abstract CryptoSymlink symlink(CryptoFolder cryptoParent, String cleartextName, String target) throws BackendException;
|
|
||||||
|
|
||||||
abstract CryptoFolder create(CryptoFolder folder) throws BackendException;
|
|
||||||
|
|
||||||
abstract CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException;
|
|
||||||
|
|
||||||
abstract CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException;
|
|
||||||
|
|
||||||
abstract void delete(CloudNode node) throws BackendException;
|
|
||||||
|
|
||||||
abstract CryptoFile write(CryptoFile cryptoFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException;
|
|
||||||
|
|
||||||
abstract String loadDirId(CryptoFolder folder) throws BackendException, EmptyDirFileException;
|
|
||||||
|
|
||||||
abstract DirIdInfo createDirIdInfo(CryptoFolder folder) throws BackendException;
|
|
||||||
|
|
||||||
private String dirHash(String directoryId) {
|
|
||||||
return cryptor().fileNameCryptor().hashDirectoryId(directoryId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFolder dataFolder() throws BackendException {
|
|
||||||
return cloudContentRepository.folder(storageLocation, DATA_DIR_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
String path(CloudFolder base, String name) {
|
|
||||||
return base.getPath() + "/" + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
File getInternalCache() {
|
|
||||||
return context.getCacheDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<CryptoFolder> deepCollectSubfolders(CryptoFolder source) throws BackendException {
|
|
||||||
Queue<CryptoFolder> queue = new LinkedList<>();
|
|
||||||
queue.add(source);
|
|
||||||
|
|
||||||
List<CryptoFolder> result = new LinkedList<>();
|
|
||||||
while (!queue.isEmpty()) {
|
|
||||||
CryptoFolder folder = queue.remove();
|
|
||||||
List<CryptoFolder> subfolders = shallowCollectSubfolders(folder);
|
|
||||||
queue.addAll(subfolders);
|
|
||||||
result.addAll(subfolders);
|
|
||||||
}
|
|
||||||
|
|
||||||
Collections.reverse(result);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<CryptoFolder> shallowCollectSubfolders(CryptoFolder source) throws BackendException {
|
|
||||||
List<CryptoFolder> result = new LinkedList<>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
List<CryptoNode> list = list(source);
|
|
||||||
for (CloudNode node : list) {
|
|
||||||
if (node instanceof CryptoFolder) {
|
|
||||||
result.add((CryptoFolder) node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (NoDirFileException e) {
|
|
||||||
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RootCryptoFolder root(CryptoCloud cryptoCloud) throws BackendException {
|
|
||||||
if (root == null) {
|
|
||||||
root = new RootCryptoFolder(cryptoCloud);
|
|
||||||
}
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CryptoFolder resolve(CryptoCloud cloud, String path) throws BackendException {
|
|
||||||
if (path.startsWith("/")) {
|
|
||||||
path = path.substring(1);
|
|
||||||
}
|
|
||||||
String[] names = path.split("/");
|
|
||||||
CryptoFolder folder = root(cloud);
|
|
||||||
for (String name : names) {
|
|
||||||
folder = folder(folder, name);
|
|
||||||
}
|
|
||||||
return folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CryptoFile file(CryptoFolder cryptoParent, String cleartextName) throws BackendException {
|
|
||||||
return file(cryptoParent, cleartextName, Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public CryptoFile file(CryptoFolder cryptoParent, String cleartextName, Optional<Long> cleartextSize) throws BackendException {
|
|
||||||
String ciphertextName = encryptFileName(cryptoParent, cleartextName);
|
|
||||||
return file(cryptoParent, cleartextName, ciphertextName, cleartextSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CryptoFile file(CryptoFolder cryptoParent, String cleartextName, String ciphertextName, Optional<Long> cleartextSize) throws BackendException {
|
|
||||||
Optional<Long> ciphertextSize;
|
|
||||||
if (cleartextSize.isPresent()) {
|
|
||||||
ciphertextSize = Optional.of(Cryptors.ciphertextSize(cleartextSize.get(), cryptor()) + cryptor().fileHeaderCryptor().headerSize());
|
|
||||||
} else {
|
|
||||||
ciphertextSize = Optional.empty();
|
|
||||||
}
|
|
||||||
CloudFile cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).getCloudFolder(), ciphertextName, ciphertextSize);
|
|
||||||
return file(cryptoParent, cleartextName, cloudFile, cleartextSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
CryptoFile file(CryptoFile cryptoFile, CloudFile cloudFile, Optional<Long> cleartextSize) throws BackendException {
|
|
||||||
return file(cryptoFile.getParent(), cryptoFile.getName(), cloudFile, cleartextSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
CryptoFile file(CryptoFolder cryptoParent, String cleartextName, CloudFile cloudFile, Optional<Long> cleartextSize) throws BackendException {
|
|
||||||
return new CryptoFile(cryptoParent, cleartextName, path(cryptoParent, cleartextName), cleartextSize, cloudFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String encryptFileName(CryptoFolder cryptoParent, String name) throws BackendException {
|
|
||||||
return encryptName(cryptoParent, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName, CloudFile dirFile) throws BackendException {
|
|
||||||
return new CryptoFolder(cryptoParent, cleartextName, path(cryptoParent, cleartextName), dirFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
CryptoFolder folder(CryptoFolder cryptoFolder, CloudFile dirFile) throws BackendException {
|
|
||||||
return new CryptoFolder(cryptoFolder.getParent(), cryptoFolder.getName(), cryptoFolder.getPath(), dirFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean exists(CloudNode node) throws BackendException {
|
|
||||||
if (node instanceof CryptoFolder) {
|
|
||||||
return exists((CryptoFolder) node);
|
|
||||||
} else if (node instanceof CryptoFile) {
|
|
||||||
return exists((CryptoFile) node);
|
|
||||||
} else if (node instanceof CryptoSymlink) {
|
|
||||||
return exists((CryptoSymlink) node);
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Unexpected CloudNode type: " + node.getClass());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean exists(CryptoFolder folder) throws BackendException {
|
|
||||||
return cloudContentRepository.exists(folder.getDirFile()) && cloudContentRepository.exists(dirIdInfo(folder).getCloudFolder());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean exists(CryptoFile file) throws BackendException {
|
|
||||||
return cloudContentRepository.exists(file.getCloudFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean exists(CryptoSymlink symlink) throws BackendException {
|
|
||||||
return cloudContentRepository.exists(symlink.getCloudFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
void assertCryptoFolderAlreadyExists(CryptoFolder cryptoFolder) throws BackendException {
|
|
||||||
if (cloudContentRepository.exists(cryptoFolder.getDirFile()) //
|
|
||||||
|| cloudContentRepository.exists(file(cryptoFolder.getParent(), cryptoFolder.getName()))) {
|
|
||||||
throw new CloudNodeAlreadyExistsException(cryptoFolder.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void assertCryptoFileAlreadyExists(CryptoFile cryptoFile) throws BackendException {
|
|
||||||
if (cloudContentRepository.exists(cryptoFile.getCloudFile()) //
|
|
||||||
|| cloudContentRepository.exists(folder(cryptoFile.getParent(), cryptoFile.getName()).getDirFile())) {
|
|
||||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CryptoFile writeFromTmpFile(DataSource originalDataSource, final CryptoFile cryptoFile, File encryptedFile, final ProgressAware<UploadState> progressAware, boolean replace) throws BackendException, IOException {
|
|
||||||
CryptoFile targetFile = targetFile(cryptoFile, replace);
|
|
||||||
return file(targetFile, //
|
|
||||||
cloudContentRepository.write( //
|
|
||||||
targetFile.getCloudFile(), //
|
|
||||||
originalDataSource.decorate(FileBasedDataSource.from(encryptedFile)), //
|
|
||||||
new UploadFileReplacingProgressAware(cryptoFile, progressAware), //
|
|
||||||
replace, //
|
|
||||||
encryptedFile.length()), //
|
|
||||||
cryptoFile.getSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
private CryptoFile targetFile(CryptoFile cryptoFile, boolean replace) throws BackendException {
|
|
||||||
if (replace || !cloudContentRepository.exists(cryptoFile)) {
|
|
||||||
return cryptoFile;
|
|
||||||
}
|
|
||||||
return firstNonExistingAutoRenamedFile(cryptoFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CryptoFile firstNonExistingAutoRenamedFile(CryptoFile original) throws BackendException {
|
|
||||||
String name = original.getName();
|
|
||||||
String nameWithoutExtension = nameWithoutExtension(name);
|
|
||||||
String extension = extension(name);
|
|
||||||
int counter = 1;
|
|
||||||
CryptoFile result;
|
|
||||||
do {
|
|
||||||
String newFileName = nameWithoutExtension + " (" + counter + ")" + extension;
|
|
||||||
result = file(original.getParent(), newFileName, original.getSize());
|
|
||||||
counter++;
|
|
||||||
} while (cloudContentRepository.exists(result));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
String nameWithoutExtension(String name) {
|
|
||||||
int lastDot = name.lastIndexOf(".");
|
|
||||||
if (lastDot == -1) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
return name.substring(0, lastDot);
|
|
||||||
}
|
|
||||||
|
|
||||||
String extension(String name) {
|
|
||||||
int lastDot = name.lastIndexOf(".");
|
|
||||||
if (lastDot == -1) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return name.substring(lastDot + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void read(CryptoFile cryptoFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
|
||||||
CloudFile ciphertextFile = cryptoFile.getCloudFile();
|
|
||||||
try {
|
|
||||||
File encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware);
|
|
||||||
progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile)));
|
|
||||||
try (ReadableByteChannel readableByteChannel = Channels.newChannel(new FileInputStream(encryptedTmpFile)); //
|
|
||||||
ReadableByteChannel decryptingReadableByteChannel = new DecryptingReadableByteChannel(readableByteChannel, cryptor(), true)) {
|
|
||||||
ByteBuffer buff = ByteBuffer.allocate(cryptor().fileContentCryptor().ciphertextChunkSize());
|
|
||||||
long cleartextSize = cryptoFile.getSize().orElse(Long.MAX_VALUE);
|
|
||||||
long decrypted = 0;
|
|
||||||
int read;
|
|
||||||
while ((read = decryptingReadableByteChannel.read(buff)) > 0) {
|
|
||||||
buff.flip();
|
|
||||||
data.write(buff.array(), 0, buff.remaining());
|
|
||||||
decrypted += read;
|
|
||||||
progressAware.onProgress(progress(DownloadState.decryption(cryptoFile)).between(0).and(cleartextSize).withValue(decrypted));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
encryptedTmpFile.delete();
|
|
||||||
progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile)));
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private File readToTmpFile(CryptoFile cryptoFile, CloudFile file, ProgressAware progressAware) throws BackendException, IOException {
|
|
||||||
File encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", getInternalCache());
|
|
||||||
try (OutputStream encryptedData = new FileOutputStream(encryptedTmpFile)) {
|
|
||||||
cloudContentRepository.read(file, Optional.of(encryptedTmpFile), encryptedData, new DownloadFileReplacingProgressAware(cryptoFile, progressAware));
|
|
||||||
return encryptedTmpFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String currentAccount(Cloud cloud) throws BackendException {
|
|
||||||
return cloudContentRepository.checkAuthenticationAndRetrieveCurrentAccount(cloud);
|
|
||||||
}
|
|
||||||
|
|
||||||
DirIdInfo dirIdInfo(CryptoFolder folder) throws BackendException {
|
|
||||||
DirIdInfo dirIdInfo = dirIdCache.get(folder);
|
|
||||||
if (dirIdInfo == null) {
|
|
||||||
return createDirIdInfo(folder);
|
|
||||||
}
|
|
||||||
return dirIdInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
DirIdInfo createDirIdInfoFor(String dirId) throws BackendException {
|
|
||||||
String dirHash = dirHash(dirId);
|
|
||||||
CloudFolder lvl2Dir = lvl2Dir(dirHash);
|
|
||||||
return new DirIdInfo(dirId, lvl2Dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] loadContentsOfDirFile(CryptoFolder folder) throws BackendException, EmptyDirFileException {
|
|
||||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
|
||||||
cloudContentRepository.read(folder.getDirFile(), Optional.empty(), out, NO_OP_PROGRESS_AWARE);
|
|
||||||
if (dirfileIsEmpty(out)) {
|
|
||||||
throw new EmptyDirFileException(folder.getName(), folder.getDirFile().getPath());
|
|
||||||
}
|
|
||||||
return out.toByteArray();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String newDirId() {
|
|
||||||
return UUID.randomUUID().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean dirfileIsEmpty(ByteArrayOutputStream out) {
|
|
||||||
return out.size() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFolder lvl2Dir(String dirHash) throws BackendException {
|
|
||||||
return cloudContentRepository.folder(lvl1Dir(dirHash), dirHash.substring(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFolder lvl1Dir(String dirHash) throws BackendException {
|
|
||||||
return cloudContentRepository.folder(dataFolder(), dirHash.substring(0, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
Cryptor cryptor() {
|
|
||||||
return cryptor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
CloudFolder storageLocation() {
|
|
||||||
return storageLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addFolderToCache(CryptoFolder result, DirIdCache.DirIdInfo dirInfo) {
|
|
||||||
dirIdCache.put(result, dirInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
void evictFromCache(CryptoFolder cryptoFolder) {
|
|
||||||
dirIdCache.evict(cryptoFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
CryptoFile writeShortNameFile(CryptoFile cryptoFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException {
|
|
||||||
if (!replace) {
|
|
||||||
assertCryptoFileAlreadyExists(cryptoFile);
|
|
||||||
}
|
|
||||||
try (InputStream stream = data.open(context)) {
|
|
||||||
File encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", getInternalCache());
|
|
||||||
try (WritableByteChannel writableByteChannel = Channels.newChannel(new FileOutputStream(encryptedTmpFile)); //
|
|
||||||
WritableByteChannel encryptingWritableByteChannel = new EncryptingWritableByteChannel(writableByteChannel, cryptor())) {
|
|
||||||
progressAware.onProgress(Progress.started(UploadState.encryption(cryptoFile)));
|
|
||||||
ByteBuffer buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize());
|
|
||||||
long ciphertextSize = Cryptors.ciphertextSize(cryptoFile.getSize().get(), cryptor()) + cryptor().fileHeaderCryptor().headerSize();
|
|
||||||
int read;
|
|
||||||
long encrypted = 0;
|
|
||||||
while ((read = stream.read(buff.array())) > 0) {
|
|
||||||
buff.limit(read);
|
|
||||||
int written = encryptingWritableByteChannel.write(buff);
|
|
||||||
buff.flip();
|
|
||||||
encrypted += written;
|
|
||||||
progressAware.onProgress(progress(UploadState.encryption(cryptoFile)).between(0).and(ciphertextSize).withValue(encrypted));
|
|
||||||
}
|
|
||||||
encryptingWritableByteChannel.close();
|
|
||||||
progressAware.onProgress(Progress.completed(UploadState.encryption(cryptoFile)));
|
|
||||||
return writeFromTmpFile(data, cryptoFile, encryptedTmpFile, progressAware, replace);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
encryptedTmpFile.delete();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,454 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.cryptomator.cryptolib.api.Cryptor
|
||||||
|
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel
|
||||||
|
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel
|
||||||
|
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
|
||||||
|
import org.cryptomator.domain.exception.EmptyDirFileException
|
||||||
|
import org.cryptomator.domain.exception.FatalBackendException
|
||||||
|
import org.cryptomator.domain.exception.NoDirFileException
|
||||||
|
import org.cryptomator.domain.exception.ParentFolderIsNullException
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository
|
||||||
|
import org.cryptomator.domain.usecases.DownloadFileReplacingProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DownloadState
|
||||||
|
import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from
|
||||||
|
import org.cryptomator.domain.usecases.cloud.Progress
|
||||||
|
import org.cryptomator.domain.usecases.cloud.UploadState
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.Channels
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.Queue
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
|
|
||||||
|
abstract class CryptoImplDecorator(
|
||||||
|
val context: Context,
|
||||||
|
private val cryptor: Supplier<Cryptor>,
|
||||||
|
val cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>,
|
||||||
|
private val storageLocation: CloudFolder,
|
||||||
|
val dirIdCache: DirIdCache,
|
||||||
|
val shorteningThreshold: Int
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var root: RootCryptoFolder? = null
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
abstract fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder
|
||||||
|
|
||||||
|
abstract fun decryptName(dirId: String, encryptedName: String): String?
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
abstract fun encryptName(cryptoParent: CryptoFolder, name: String): String
|
||||||
|
|
||||||
|
abstract fun extractEncryptedName(ciphertextName: String): String?
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
abstract fun list(cryptoFolder: CryptoFolder): List<CryptoNode>
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
abstract fun encryptFolderName(cryptoFolder: CryptoFolder, name: String): String
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
abstract fun symlink(cryptoParent: CryptoFolder, cleartextName: String, target: String): CryptoSymlink
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
abstract fun create(folder: CryptoFolder): CryptoFolder
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
abstract fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
abstract fun move(source: CryptoFile, target: CryptoFile): CryptoFile
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
abstract fun delete(node: CloudNode)
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
abstract fun write(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile
|
||||||
|
|
||||||
|
@Throws(BackendException::class, EmptyDirFileException::class)
|
||||||
|
abstract fun loadDirId(folder: CryptoFolder): String
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
abstract fun createDirIdInfo(folder: CryptoFolder): DirIdInfo
|
||||||
|
|
||||||
|
private fun dirHash(directoryId: String): String {
|
||||||
|
return cryptor().fileNameCryptor().hashDirectoryId(directoryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun dataFolder(): CloudFolder {
|
||||||
|
return cloudContentRepository.folder(storageLocation, CryptoConstants.DATA_DIR_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun path(base: CloudFolder, name: String): String {
|
||||||
|
return base.path + "/" + name
|
||||||
|
}
|
||||||
|
|
||||||
|
val internalCache: File
|
||||||
|
get() = context.cacheDir
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun deepCollectSubfolders(source: CryptoFolder): List<CryptoFolder> {
|
||||||
|
|
||||||
|
val queue: Queue<CryptoFolder> = LinkedList()
|
||||||
|
queue.add(source)
|
||||||
|
val result: MutableList<CryptoFolder> = LinkedList()
|
||||||
|
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
val folder = queue.remove()
|
||||||
|
val subfolders = shallowCollectSubfolders(folder)
|
||||||
|
queue.addAll(subfolders)
|
||||||
|
result.addAll(subfolders)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.reverse()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun shallowCollectSubfolders(source: CryptoFolder): List<CryptoFolder> {
|
||||||
|
return try {
|
||||||
|
list(source).filterIsInstance<CryptoFolder>()
|
||||||
|
} catch (e: NoDirFileException) {
|
||||||
|
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
@Synchronized
|
||||||
|
fun root(cryptoCloud: CryptoCloud): RootCryptoFolder = root ?: RootCryptoFolder(cryptoCloud).also { root = it }
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun resolve(cloud: CryptoCloud, path: String): CryptoFolder {
|
||||||
|
val names = path.removePrefix("/").split("/").toTypedArray()
|
||||||
|
var folder: CryptoFolder = root(cloud)
|
||||||
|
for (name in names) {
|
||||||
|
folder = folder(folder, name)
|
||||||
|
}
|
||||||
|
return folder
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun file(cryptoParent: CryptoFolder, cleartextName: String): CryptoFile {
|
||||||
|
return file(cryptoParent, cleartextName, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun file(cryptoParent: CryptoFolder, cleartextName: String, cleartextSize: Long?): CryptoFile {
|
||||||
|
val ciphertextName = encryptFileName(cryptoParent, cleartextName)
|
||||||
|
return file(cryptoParent, cleartextName, ciphertextName, cleartextSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun file(cryptoParent: CryptoFolder, cleartextName: String, ciphertextName: String, cleartextSize: Long?): CryptoFile {
|
||||||
|
val ciphertextSize = cleartextSize?.let { cryptor().fileContentCryptor().ciphertextSize(it) + cryptor().fileHeaderCryptor().headerSize() }
|
||||||
|
val cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, ciphertextName, ciphertextSize)
|
||||||
|
return file(cryptoParent, cleartextName, cloudFile, cleartextSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun file(cryptoFile: CryptoFile, cloudFile: CloudFile, cleartextSize: Long?): CryptoFile {
|
||||||
|
return file(cryptoFile.parent, cryptoFile.name, cloudFile, cleartextSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun file(cryptoParent: CryptoFolder, cleartextName: String, cloudFile: CloudFile, cleartextSize: Long?): CryptoFile {
|
||||||
|
return CryptoFile(cryptoParent, cleartextName, path(cryptoParent, cleartextName), cleartextSize, cloudFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun encryptFileName(cryptoParent: CryptoFolder, name: String): String {
|
||||||
|
return encryptName(cryptoParent, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun folder(cryptoParent: CryptoFolder, cleartextName: String, dirFile: CloudFile): CryptoFolder {
|
||||||
|
return CryptoFolder(cryptoParent, cleartextName, path(cryptoParent, cleartextName), dirFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun folder(cryptoFolder: CryptoFolder, dirFile: CloudFile): CryptoFolder {
|
||||||
|
return CryptoFolder(cryptoFolder.parent, cryptoFolder.name, cryptoFolder.path, dirFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun exists(node: CloudNode): Boolean {
|
||||||
|
return when (node) {
|
||||||
|
is CryptoFolder -> {
|
||||||
|
exists(node)
|
||||||
|
}
|
||||||
|
is CryptoFile -> {
|
||||||
|
exists(node)
|
||||||
|
}
|
||||||
|
is CryptoSymlink -> {
|
||||||
|
exists(node)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw IllegalArgumentException("Unexpected CloudNode type: " + node.javaClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun exists(folder: CryptoFolder): Boolean {
|
||||||
|
requireNotNull(folder.dirFile)
|
||||||
|
return cloudContentRepository.exists(folder.dirFile) && cloudContentRepository.exists(dirIdInfo(folder).cloudFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun exists(file: CryptoFile): Boolean {
|
||||||
|
return cloudContentRepository.exists(file.cloudFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun exists(symlink: CryptoSymlink): Boolean {
|
||||||
|
return cloudContentRepository.exists(symlink.cloudFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun assertCryptoFolderAlreadyExists(cryptoFolder: CryptoFolder) {
|
||||||
|
requireNotNull(cryptoFolder.dirFile)
|
||||||
|
requireNotNull(cryptoFolder.parent)
|
||||||
|
cryptoFolder.parent?.let { cryptosParent ->
|
||||||
|
if (cloudContentRepository.exists(cryptoFolder.dirFile)
|
||||||
|
|| cloudContentRepository.exists(file(cryptosParent, cryptoFolder.name))
|
||||||
|
) {
|
||||||
|
throw CloudNodeAlreadyExistsException(cryptoFolder.name)
|
||||||
|
}
|
||||||
|
} ?: throw ParentFolderIsNullException(cryptoFolder.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun assertCryptoFileAlreadyExists(cryptoFile: CryptoFile) {
|
||||||
|
val dirFile = folder(cryptoFile.parent, cryptoFile.name).dirFile
|
||||||
|
requireNotNull(dirFile)
|
||||||
|
if (cloudContentRepository.exists(cryptoFile.cloudFile) //
|
||||||
|
|| cloudContentRepository.exists(dirFile)
|
||||||
|
) {
|
||||||
|
throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class, IOException::class)
|
||||||
|
private fun writeFromTmpFile(originalDataSource: DataSource, cryptoFile: CryptoFile, encryptedFile: File, progressAware: ProgressAware<UploadState>, replace: Boolean): CryptoFile {
|
||||||
|
val targetFile = targetFile(cryptoFile, replace)
|
||||||
|
return file(
|
||||||
|
targetFile, //
|
||||||
|
cloudContentRepository.write( //
|
||||||
|
targetFile.cloudFile, //
|
||||||
|
originalDataSource.decorate(from(encryptedFile)), //
|
||||||
|
UploadFileReplacingProgressAware(cryptoFile, progressAware), //
|
||||||
|
replace, //
|
||||||
|
encryptedFile.length()
|
||||||
|
), //
|
||||||
|
cryptoFile.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun targetFile(cryptoFile: CryptoFile, replace: Boolean): CryptoFile {
|
||||||
|
return if (replace || !cloudContentRepository.exists(cryptoFile)) {
|
||||||
|
cryptoFile
|
||||||
|
} else firstNonExistingAutoRenamedFile(cryptoFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun firstNonExistingAutoRenamedFile(original: CryptoFile): CryptoFile {
|
||||||
|
val name = original.name
|
||||||
|
val nameWithoutExtension = nameWithoutExtension(name)
|
||||||
|
val extension = extension(name)
|
||||||
|
var counter = 1
|
||||||
|
var result: CryptoFile
|
||||||
|
do {
|
||||||
|
val newFileName = "$nameWithoutExtension ($counter)$extension"
|
||||||
|
result = file(original.parent, newFileName, original.size)
|
||||||
|
counter++
|
||||||
|
} while (cloudContentRepository.exists(result))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nameWithoutExtension(name: String): String {
|
||||||
|
val lastDot = name.lastIndexOf(".")
|
||||||
|
return if (lastDot == -1) {
|
||||||
|
name
|
||||||
|
} else name.substring(0, lastDot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extension(name: String): String {
|
||||||
|
val lastDot = name.lastIndexOf(".")
|
||||||
|
return if (lastDot == -1) {
|
||||||
|
""
|
||||||
|
} else name.substring(lastDot + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun read(cryptoFile: CryptoFile, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
|
||||||
|
val ciphertextFile = cryptoFile.cloudFile
|
||||||
|
try {
|
||||||
|
val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware)
|
||||||
|
progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile)))
|
||||||
|
try {
|
||||||
|
Channels.newChannel(FileInputStream(encryptedTmpFile)).use { readableByteChannel ->
|
||||||
|
DecryptingReadableByteChannel(readableByteChannel, cryptor(), true).use { decryptingReadableByteChannel ->
|
||||||
|
val buff = ByteBuffer.allocate(cryptor().fileContentCryptor().ciphertextChunkSize())
|
||||||
|
val cleartextSize = cryptoFile.size ?: Long.MAX_VALUE
|
||||||
|
var decrypted: Long = 0
|
||||||
|
var read: Int
|
||||||
|
while (decryptingReadableByteChannel.read(buff).also { read = it } > 0) {
|
||||||
|
buff.flip()
|
||||||
|
data.write(buff.array(), 0, buff.remaining())
|
||||||
|
decrypted += read.toLong()
|
||||||
|
progressAware
|
||||||
|
.onProgress(
|
||||||
|
Progress.progress(DownloadState.decryption(cryptoFile)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(cleartextSize) //
|
||||||
|
.withValue(decrypted)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
encryptedTmpFile.delete()
|
||||||
|
progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile)))
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class, IOException::class)
|
||||||
|
private fun readToTmpFile(cryptoFile: CryptoFile, file: CloudFile, progressAware: ProgressAware<DownloadState>): File {
|
||||||
|
val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache)
|
||||||
|
FileOutputStream(encryptedTmpFile).use { encryptedData ->
|
||||||
|
cloudContentRepository.read(file, encryptedTmpFile, encryptedData, DownloadFileReplacingProgressAware(cryptoFile, progressAware))
|
||||||
|
return encryptedTmpFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun currentAccount(cloud: Cloud): String {
|
||||||
|
return cloudContentRepository.checkAuthenticationAndRetrieveCurrentAccount(cloud)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun dirIdInfo(folder: CryptoFolder): DirIdInfo {
|
||||||
|
return dirIdCache[folder] ?: return createDirIdInfo(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun createDirIdInfoFor(dirId: String): DirIdInfo {
|
||||||
|
val dirHash = dirHash(dirId)
|
||||||
|
val lvl2Dir = lvl2Dir(dirHash)
|
||||||
|
return DirIdInfo(dirId, lvl2Dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class, EmptyDirFileException::class)
|
||||||
|
fun loadContentsOfDirFile(folder: CryptoFolder): ByteArray {
|
||||||
|
folder.dirFile?.let {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream().use { out ->
|
||||||
|
cloudContentRepository.read(it, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
|
||||||
|
if (dirfileIsEmpty(out)) {
|
||||||
|
throw EmptyDirFileException(folder.name, folder.dirFile.path)
|
||||||
|
}
|
||||||
|
return out.toByteArray()
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
} ?: throw FatalBackendException("Dir file is null")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newDirId(): String {
|
||||||
|
return UUID.randomUUID().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dirfileIsEmpty(out: ByteArrayOutputStream): Boolean {
|
||||||
|
return out.size() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun lvl2Dir(dirHash: String): CloudFolder {
|
||||||
|
return cloudContentRepository.folder(lvl1Dir(dirHash), dirHash.substring(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun lvl1Dir(dirHash: String): CloudFolder {
|
||||||
|
return cloudContentRepository.folder(dataFolder(), dirHash.substring(0, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cryptor(): Cryptor {
|
||||||
|
return cryptor.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun storageLocation(): CloudFolder {
|
||||||
|
return storageLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addFolderToCache(result: CryptoFolder, dirInfo: DirIdInfo) {
|
||||||
|
dirIdCache.put(result, dirInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun evictFromCache(cryptoFolder: CryptoFolder) {
|
||||||
|
dirIdCache.evict(cryptoFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun writeShortNameFile(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile {
|
||||||
|
if (!replace) {
|
||||||
|
assertCryptoFileAlreadyExists(cryptoFile)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
data.open(context)?.use { stream ->
|
||||||
|
requireNotNull(cryptoFile.size)
|
||||||
|
val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache)
|
||||||
|
try {
|
||||||
|
Channels.newChannel(FileOutputStream(encryptedTmpFile)).use { writableByteChannel ->
|
||||||
|
EncryptingWritableByteChannel(writableByteChannel, cryptor()).use { encryptingWritableByteChannel ->
|
||||||
|
progressAware.onProgress(Progress.started(UploadState.encryption(cryptoFile)))
|
||||||
|
val buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize())
|
||||||
|
val ciphertextSize = cryptor().fileContentCryptor().ciphertextSize(cryptoFile.size) + cryptor().fileHeaderCryptor().headerSize()
|
||||||
|
var read: Int
|
||||||
|
var encrypted: Long = 0
|
||||||
|
while (stream.read(buff.array()).also { read = it } > 0) {
|
||||||
|
buff.limit(read)
|
||||||
|
val written = encryptingWritableByteChannel.write(buff)
|
||||||
|
buff.flip()
|
||||||
|
encrypted += written.toLong()
|
||||||
|
progressAware.onProgress(Progress.progress(UploadState.encryption(cryptoFile)).between(0).and(ciphertextSize).withValue(encrypted))
|
||||||
|
}
|
||||||
|
encryptingWritableByteChannel.close()
|
||||||
|
progressAware.onProgress(Progress.completed(UploadState.encryption(cryptoFile)))
|
||||||
|
return writeFromTmpFile(data, cryptoFile, encryptedTmpFile, progressAware, replace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
encryptedTmpFile.delete()
|
||||||
|
}
|
||||||
|
} ?: throw IllegalStateException("InputStream shouldn't be null")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,546 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding;
|
|
||||||
|
|
||||||
import org.cryptomator.cryptolib.Cryptors;
|
|
||||||
import org.cryptomator.cryptolib.EncryptingWritableByteChannel;
|
|
||||||
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
|
|
||||||
import org.cryptomator.cryptolib.api.Cryptor;
|
|
||||||
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
|
|
||||||
import org.cryptomator.domain.exception.EmptyDirFileException;
|
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
|
||||||
import org.cryptomator.domain.exception.NoDirFileException;
|
|
||||||
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
|
||||||
import org.cryptomator.domain.exception.SymLinkException;
|
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.FileBasedDataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.Progress;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
import org.cryptomator.util.Supplier;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.channels.Channels;
|
|
||||||
import java.nio.channels.WritableByteChannel;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
|
||||||
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
|
|
||||||
import static org.cryptomator.util.Encodings.UTF_8;
|
|
||||||
|
|
||||||
final class CryptoImplVaultFormat7 extends CryptoImplDecorator {
|
|
||||||
|
|
||||||
private static final int SHORT_NAMES_MAX_LENGTH = 220;
|
|
||||||
private static final String CLOUD_NODE_EXT = ".c9r";
|
|
||||||
private static final String LONG_NODE_FILE_EXT = ".c9s";
|
|
||||||
private static final String CLOUD_FOLDER_DIR_FILE_PRE = "dir";
|
|
||||||
private static final String LONG_NODE_FILE_CONTENT_CONTENTS = "contents";
|
|
||||||
private static final String LONG_NODE_FILE_CONTENT_NAME = "name";
|
|
||||||
private static final String CLOUD_NODE_SYMLINK_PRE = "symlink";
|
|
||||||
private static final Pattern BASE64_ENCRYPTED_NAME_PATTERN = Pattern.compile("^([A-Za-z0-9+/\\-_]{4})*([A-Za-z0-9+/\\-]{4}|[A-Za-z0-9+/\\-_]{3}=|[A-Za-z0-9+/\\-_]{2}==)?$");
|
|
||||||
|
|
||||||
private static final BaseEncoding BASE64 = BaseEncoding.base64Url();
|
|
||||||
|
|
||||||
CryptoImplVaultFormat7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
|
|
||||||
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException {
|
|
||||||
String dirFileName = encryptFolderName(cryptoParent, cleartextName);
|
|
||||||
CloudFolder dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).getCloudFolder(), dirFileName);
|
|
||||||
CloudFile dirFile = cloudContentRepository.file(dirFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT);
|
|
||||||
return folder(cryptoParent, cleartextName, dirFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String encryptName(CryptoFolder cryptoFolder, String name) throws BackendException {
|
|
||||||
String ciphertextName = cryptor() //
|
|
||||||
.fileNameCryptor() //
|
|
||||||
.encryptFilename(BASE64, name, dirIdInfo(cryptoFolder).getId().getBytes(UTF_8)) + CLOUD_NODE_EXT;
|
|
||||||
|
|
||||||
if (ciphertextName.length() > SHORT_NAMES_MAX_LENGTH) {
|
|
||||||
ciphertextName = deflate(cryptoFolder, ciphertextName);
|
|
||||||
}
|
|
||||||
return ciphertextName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String deflate(CryptoFolder cryptoParent, String longFileName) throws BackendException {
|
|
||||||
byte[] longFilenameBytes = longFileName.getBytes(UTF_8);
|
|
||||||
byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes);
|
|
||||||
String shortFileName = BASE64.encode(hash) + LONG_NODE_FILE_EXT;
|
|
||||||
|
|
||||||
CloudFolder dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).getCloudFolder(), shortFileName);
|
|
||||||
|
|
||||||
// if folder already exists in case of renaming
|
|
||||||
if (!cloudContentRepository.exists(dirFolder)) {
|
|
||||||
dirFolder = cloudContentRepository.create(dirFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] data = longFileName.getBytes(UTF_8);
|
|
||||||
CloudFile cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT, Optional.of((long) data.length));
|
|
||||||
cloudContentRepository.write(cloudFile, ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, true, data.length);
|
|
||||||
return shortFileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFile metadataFile(CloudNode cloudNode) throws BackendException {
|
|
||||||
CloudFolder cloudFolder;
|
|
||||||
|
|
||||||
if (cloudNode instanceof CloudFile) {
|
|
||||||
cloudFolder = cloudNode.getParent();
|
|
||||||
} else if (cloudNode instanceof CloudFolder) {
|
|
||||||
cloudFolder = (CloudFolder) cloudNode;
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("Should be file or folder");
|
|
||||||
}
|
|
||||||
|
|
||||||
return cloudContentRepository.file(cloudFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String inflate(CloudNode cloudNode) throws BackendException {
|
|
||||||
CloudFile metadataFile = metadataFile(cloudNode);
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
||||||
cloudContentRepository.read(metadataFile, Optional.empty(), out, NO_OP_PROGRESS_AWARE);
|
|
||||||
return new String(out.toByteArray(), UTF_8);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String decryptName(String dirId, String encryptedName) {
|
|
||||||
Optional<String> ciphertextName = extractEncryptedName(encryptedName);
|
|
||||||
if (ciphertextName.isPresent()) {
|
|
||||||
return cryptor().fileNameCryptor().decryptFilename(BASE64, ciphertextName.get(), dirId.getBytes(UTF_8));
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
List<CryptoNode> list(CryptoFolder cryptoFolder) throws BackendException {
|
|
||||||
dirIdCache.evictSubFoldersOf(cryptoFolder);
|
|
||||||
|
|
||||||
DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(cryptoFolder);
|
|
||||||
String dirId = dirIdInfo(cryptoFolder).getId();
|
|
||||||
CloudFolder lvl2Dir = dirIdInfo.getCloudFolder();
|
|
||||||
|
|
||||||
List<CloudNode> ciphertextNodes;
|
|
||||||
|
|
||||||
try {
|
|
||||||
ciphertextNodes = cloudContentRepository.list(lvl2Dir);
|
|
||||||
} catch (NoSuchCloudFileException e) {
|
|
||||||
if (cryptoFolder instanceof RootCryptoFolder) {
|
|
||||||
Timber.tag("CryptoFs").e("No lvl2Dir exists for root folder in %s", lvl2Dir.getPath());
|
|
||||||
throw new FatalBackendException(String.format("No lvl2Dir exists for root folder in %s", lvl2Dir.getPath()), e);
|
|
||||||
} else if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder.getDirFile().getParent(), CLOUD_NODE_SYMLINK_PRE + CLOUD_NODE_EXT))) {
|
|
||||||
throw new SymLinkException();
|
|
||||||
} else if (!cloudContentRepository.exists(cryptoFolder.getDirFile())) {
|
|
||||||
Timber.tag("CryptoFs").e("No dir file exists in %s", cryptoFolder.getDirFile().getPath());
|
|
||||||
throw new NoDirFileException(cryptoFolder.getName(), cryptoFolder.getDirFile().getPath());
|
|
||||||
}
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<CryptoNode> result = new ArrayList<>();
|
|
||||||
for (CloudNode node : ciphertextNodes) {
|
|
||||||
ciphertextToCleartextNode(cryptoFolder, dirId, node).ifPresent(result::add);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<CryptoNode> ciphertextToCleartextNode(CryptoFolder cryptoFolder, String dirId, CloudNode cloudNode) throws BackendException {
|
|
||||||
String ciphertextName = cloudNode.getName();
|
|
||||||
Optional<CloudFile> longNameFolderDirFile = Optional.empty();
|
|
||||||
Optional<CloudFile> longNameFile = Optional.empty();
|
|
||||||
|
|
||||||
if (ciphertextName.endsWith(CLOUD_NODE_EXT)) {
|
|
||||||
ciphertextName = nameWithoutExtension(ciphertextName);
|
|
||||||
} else if (ciphertextName.endsWith(LONG_NODE_FILE_EXT)) {
|
|
||||||
Optional<String> ciphertextNameOption = longNodeCiphertextName(cloudNode);
|
|
||||||
if (ciphertextNameOption.isPresent()) {
|
|
||||||
ciphertextName = ciphertextNameOption.get();
|
|
||||||
} else {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<CloudNode> subfiles = cloudContentRepository.list((CloudFolder) cloudNode);
|
|
||||||
|
|
||||||
for (CloudNode cloudNode1 : subfiles) {
|
|
||||||
switch (cloudNode1.getName()) {
|
|
||||||
case LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT:
|
|
||||||
longNameFile = Optional.of((CloudFile) cloudNode1);
|
|
||||||
break;
|
|
||||||
case CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT:
|
|
||||||
longNameFolderDirFile = Optional.of((CloudFile) cloudNode1);
|
|
||||||
break;
|
|
||||||
case CLOUD_NODE_SYMLINK_PRE + CLOUD_NODE_EXT:
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String cleartextName = decryptName(dirId, ciphertextName);
|
|
||||||
|
|
||||||
if (cleartextName == null) {
|
|
||||||
Timber.tag("CryptoFs").w("Failed to parse cipher text name of: %s", cloudNode.getPath());
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
return cloudNodeFromName(cloudNode, cryptoFolder, cleartextName, longNameFile, longNameFolderDirFile);
|
|
||||||
} catch (AuthenticationFailedException e) {
|
|
||||||
Timber.tag("CryptoFs").w(e, "File/Folder name authentication failed: %s", cloudNode.getPath());
|
|
||||||
return Optional.empty();
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
Timber.tag("CryptoFs").w(e, "Illegal ciphertext filename/folder: %s", cloudNode.getPath());
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<CryptoNode> cloudNodeFromName(CloudNode cloudNode, CryptoFolder cryptoFolder, String cleartextName, Optional<CloudFile> longNameFile, Optional<CloudFile> dirFile) throws BackendException {
|
|
||||||
if (cloudNode instanceof CloudFile) {
|
|
||||||
CloudFile cloudFile = (CloudFile) cloudNode;
|
|
||||||
Optional<Long> cleartextSize = Optional.empty();
|
|
||||||
if (cloudFile.getSize().isPresent()) {
|
|
||||||
long ciphertextSizeWithoutHeader = cloudFile.getSize().get() - cryptor().fileHeaderCryptor().headerSize();
|
|
||||||
if (ciphertextSizeWithoutHeader >= 0) {
|
|
||||||
cleartextSize = Optional.of(Cryptors.cleartextSize(ciphertextSizeWithoutHeader, cryptor()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.of(file(cryptoFolder, cleartextName, cloudFile, cleartextSize));
|
|
||||||
} else if (cloudNode instanceof CloudFolder) {
|
|
||||||
if (longNameFile.isPresent()) {
|
|
||||||
// long file
|
|
||||||
Optional<Long> cleartextSize = Optional.empty();
|
|
||||||
if (longNameFile.get().getSize().isPresent()) {
|
|
||||||
long ciphertextSizeWithoutHeader = longNameFile.get().getSize().get() - cryptor().fileHeaderCryptor().headerSize();
|
|
||||||
if (ciphertextSizeWithoutHeader >= 0) {
|
|
||||||
cleartextSize = Optional.of(Cryptors.cleartextSize(ciphertextSizeWithoutHeader, cryptor()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.of(file(cryptoFolder, cleartextName, longNameFile.get(), cleartextSize));
|
|
||||||
} else {
|
|
||||||
// folder
|
|
||||||
if (dirFile.isPresent()) {
|
|
||||||
return Optional.of(folder(cryptoFolder, cleartextName, dirFile.get()));
|
|
||||||
} else {
|
|
||||||
CloudFile constructedDirFile = cloudContentRepository.file((CloudFolder) cloudNode, "dir" + CLOUD_NODE_EXT);
|
|
||||||
return Optional.of(folder(cryptoFolder, cleartextName, constructedDirFile));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> longNodeCiphertextName(CloudNode cloudNode) {
|
|
||||||
try {
|
|
||||||
String ciphertextName = inflate(cloudNode);
|
|
||||||
ciphertextName = nameWithoutExtension(ciphertextName);
|
|
||||||
return Optional.of(ciphertextName);
|
|
||||||
} catch (NoSuchCloudFileException e) {
|
|
||||||
Timber.tag("CryptoFs").e("Missing %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.getPath());
|
|
||||||
return Optional.empty();
|
|
||||||
} catch (BackendException e) {
|
|
||||||
Timber.tag("CryptoFs").e(e, "Failed to read %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.getPath());
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
DirIdCache.DirIdInfo createDirIdInfo(CryptoFolder folder) throws BackendException {
|
|
||||||
String dirId = loadDirId(folder);
|
|
||||||
return dirIdCache.put(folder, createDirIdInfoFor(dirId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String encryptFolderName(CryptoFolder cryptoFolder, String name) throws BackendException {
|
|
||||||
return encryptName(cryptoFolder, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CryptoSymlink symlink(CryptoFolder cryptoParent, String cleartextName, String target) throws BackendException {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String loadDirId(CryptoFolder folder) throws BackendException, EmptyDirFileException {
|
|
||||||
CloudFile dirFile = null;
|
|
||||||
|
|
||||||
if (folder.getDirFile() != null) {
|
|
||||||
dirFile = folder.getDirFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RootCryptoFolder.isRoot(folder)) {
|
|
||||||
return CryptoConstants.ROOT_DIR_ID;
|
|
||||||
} else if (dirFile != null && cloudContentRepository.exists(dirFile)) {
|
|
||||||
return new String(loadContentsOfDirFile(dirFile), UTF_8);
|
|
||||||
} else {
|
|
||||||
return newDirId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] loadContentsOfDirFile(CloudFile file) throws BackendException, EmptyDirFileException {
|
|
||||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
|
||||||
cloudContentRepository.read(file, Optional.empty(), out, NO_OP_PROGRESS_AWARE);
|
|
||||||
if (dirfileIsEmpty(out)) {
|
|
||||||
throw new EmptyDirFileException(file.getName(), file.getPath());
|
|
||||||
}
|
|
||||||
return out.toByteArray();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CryptoFolder create(CryptoFolder folder) throws BackendException {
|
|
||||||
boolean shortName = false;
|
|
||||||
if (folder.getDirFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) {
|
|
||||||
assertCryptoLongDirFileAlreadyExists(folder);
|
|
||||||
} else {
|
|
||||||
assertCryptoFolderAlreadyExists(folder);
|
|
||||||
shortName = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(folder);
|
|
||||||
CloudFolder createdCloudFolder = cloudContentRepository.create(dirIdInfo.getCloudFolder());
|
|
||||||
|
|
||||||
CloudFolder dirFolder = folder.getDirFile().getParent();
|
|
||||||
CloudFile dirFile = folder.getDirFile();
|
|
||||||
if (shortName) {
|
|
||||||
dirFolder = cloudContentRepository.create(dirFolder);
|
|
||||||
dirFile = cloudContentRepository.file(dirFolder, folder.getDirFile().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] dirId = dirIdInfo.getId().getBytes(UTF_8);
|
|
||||||
CloudFile createdDirFile = cloudContentRepository.write(dirFile, ByteArrayDataSource.from(dirId), NO_OP_PROGRESS_AWARE, false, dirId.length);
|
|
||||||
CryptoFolder result = folder(folder, createdDirFile);
|
|
||||||
addFolderToCache(result, dirIdInfo.withCloudFolder(createdCloudFolder));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Optional<String> extractEncryptedName(String ciphertextName) {
|
|
||||||
final Matcher matcher = BASE64_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName);
|
|
||||||
if (matcher.find(0)) {
|
|
||||||
return Optional.of(matcher.group());
|
|
||||||
} else {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException {
|
|
||||||
boolean shortName = false;
|
|
||||||
if (target.getDirFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) {
|
|
||||||
assertCryptoLongDirFileAlreadyExists(target);
|
|
||||||
} else {
|
|
||||||
assertCryptoFolderAlreadyExists(target);
|
|
||||||
shortName = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
CloudFile targetDirFile = target.getDirFile();
|
|
||||||
if (shortName) {
|
|
||||||
CloudFolder targetDirFolder = cloudContentRepository.create(target.getDirFile().getParent());
|
|
||||||
targetDirFile = cloudContentRepository.file(targetDirFolder, target.getDirFile().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
CryptoFolder result = folder(target.getParent(), target.getName(), cloudContentRepository.move(source.getDirFile(), targetDirFile));
|
|
||||||
|
|
||||||
cloudContentRepository.delete(source.getDirFile().getParent());
|
|
||||||
|
|
||||||
evictFromCache(source);
|
|
||||||
evictFromCache(target);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException {
|
|
||||||
if (source.getCloudFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) {
|
|
||||||
CloudFolder targetDirFolder = cloudContentRepository.folder(target.getCloudFile().getParent(), target.getCloudFile().getName());
|
|
||||||
CryptoFile cryptoFile;
|
|
||||||
if (target.getCloudFile().getName().endsWith(LONG_NODE_FILE_EXT)) {
|
|
||||||
assertCryptoLongDirFileAlreadyExists(targetDirFolder);
|
|
||||||
cryptoFile = moveLongFileToLongFile(source, target, targetDirFolder);
|
|
||||||
} else {
|
|
||||||
assertCryptoFileAlreadyExists(target);
|
|
||||||
cryptoFile = moveLongFileToShortFile(source, target);
|
|
||||||
}
|
|
||||||
CloudFolder sourceDirFolder = cloudContentRepository.folder(source.getCloudFile().getParent().getParent(), source.getCloudFile().getParent().getName());
|
|
||||||
cloudContentRepository.delete(sourceDirFolder);
|
|
||||||
return cryptoFile;
|
|
||||||
} else {
|
|
||||||
CloudFolder targetDirFolder = cloudContentRepository.folder(target.getCloudFile().getParent(), target.getCloudFile().getName());
|
|
||||||
if (target.getCloudFile().getName().endsWith(LONG_NODE_FILE_EXT)) {
|
|
||||||
assertCryptoLongDirFileAlreadyExists(targetDirFolder);
|
|
||||||
return moveShortFileToLongFile(source, target, targetDirFolder);
|
|
||||||
} else {
|
|
||||||
assertCryptoFileAlreadyExists(target);
|
|
||||||
return file(target, cloudContentRepository.move(source.getCloudFile(), target.getCloudFile()), source.getSize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CryptoFile moveLongFileToLongFile(CryptoFile source, CryptoFile target, CloudFolder targetDirFolder) throws BackendException {
|
|
||||||
CloudFile sourceFile = cloudContentRepository.file(source.getCloudFile().getParent(), LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT);
|
|
||||||
CloudFile movedFile = cloudContentRepository.move(sourceFile, cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT));
|
|
||||||
return file(target, movedFile, movedFile.getSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
private CryptoFile moveLongFileToShortFile(CryptoFile source, CryptoFile target) throws BackendException {
|
|
||||||
CloudFile sourceFile = cloudContentRepository.file(source.getCloudFile().getParent(), LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT);
|
|
||||||
CloudFile movedFile = cloudContentRepository.move(sourceFile, target.getCloudFile());
|
|
||||||
return file(target, movedFile, movedFile.getSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
private CryptoFile moveShortFileToLongFile(CryptoFile source, CryptoFile target, CloudFolder targetDirFolder) throws BackendException {
|
|
||||||
CloudFile movedFile = cloudContentRepository.move(source.getCloudFile(), cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT));
|
|
||||||
return file(target, movedFile, movedFile.getSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void delete(CloudNode node) throws BackendException {
|
|
||||||
if (node instanceof CryptoFolder) {
|
|
||||||
CryptoFolder cryptoFolder = (CryptoFolder) node;
|
|
||||||
List<CryptoFolder> cryptoSubfolders = deepCollectSubfolders(cryptoFolder);
|
|
||||||
for (CryptoFolder cryptoSubfolder : cryptoSubfolders) {
|
|
||||||
try {
|
|
||||||
cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).getCloudFolder());
|
|
||||||
} catch (NoSuchCloudFileException e) {
|
|
||||||
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
cloudContentRepository.delete(dirIdInfo(cryptoFolder).getCloudFolder());
|
|
||||||
} catch (NoSuchCloudFileException e) {
|
|
||||||
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudContentRepository.delete(cryptoFolder.getDirFile().getParent());
|
|
||||||
|
|
||||||
evictFromCache(cryptoFolder);
|
|
||||||
} else if (node instanceof CryptoFile) {
|
|
||||||
CryptoFile cryptoFile = (CryptoFile) node;
|
|
||||||
if (cryptoFile.getCloudFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) {
|
|
||||||
cloudContentRepository.delete(cryptoFile.getCloudFile().getParent());
|
|
||||||
} else {
|
|
||||||
cloudContentRepository.delete(cryptoFile.getCloudFile());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFile write(CryptoFile cryptoFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException {
|
|
||||||
if (cryptoFile.getCloudFile().getName().endsWith(LONG_NODE_FILE_EXT)) {
|
|
||||||
return writeLongFile(cryptoFile, data, progressAware, replace, length);
|
|
||||||
} else {
|
|
||||||
return writeShortNameFile(cryptoFile, data, progressAware, replace, length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CryptoFile writeLongFile(CryptoFile cryptoFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException {
|
|
||||||
CloudFolder dirFolder = cloudContentRepository.folder(cryptoFile.getCloudFile().getParent(), cryptoFile.getCloudFile().getName());
|
|
||||||
CloudFile cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, data.size(context));
|
|
||||||
|
|
||||||
assertCryptoLongDirFileAlreadyExists(dirFolder);
|
|
||||||
|
|
||||||
try (InputStream stream = data.open(context)) {
|
|
||||||
File encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", getInternalCache());
|
|
||||||
try (WritableByteChannel writableByteChannel = Channels.newChannel(new FileOutputStream(encryptedTmpFile)); //
|
|
||||||
WritableByteChannel encryptingWritableByteChannel = new EncryptingWritableByteChannel(writableByteChannel, cryptor())) {
|
|
||||||
progressAware.onProgress(Progress.started(UploadState.encryption(cloudFile)));
|
|
||||||
ByteBuffer buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize());
|
|
||||||
long ciphertextSize = Cryptors.ciphertextSize(cloudFile.getSize().get(), cryptor()) + cryptor().fileHeaderCryptor().headerSize();
|
|
||||||
int read;
|
|
||||||
long encrypted = 0;
|
|
||||||
while ((read = stream.read(buff.array())) > 0) {
|
|
||||||
buff.limit(read);
|
|
||||||
int written = encryptingWritableByteChannel.write(buff);
|
|
||||||
buff.flip();
|
|
||||||
encrypted += written;
|
|
||||||
progressAware.onProgress(progress(UploadState.encryption(cloudFile)).between(0).and(ciphertextSize).withValue(encrypted));
|
|
||||||
}
|
|
||||||
encryptingWritableByteChannel.close();
|
|
||||||
progressAware.onProgress(Progress.completed(UploadState.encryption(cloudFile)));
|
|
||||||
|
|
||||||
CloudFile targetFile = targetFile(cryptoFile, cloudFile, replace);
|
|
||||||
|
|
||||||
return file(cryptoFile, //
|
|
||||||
cloudContentRepository.write( //
|
|
||||||
targetFile, //
|
|
||||||
data.decorate(FileBasedDataSource.from(encryptedTmpFile)), //
|
|
||||||
new UploadFileReplacingProgressAware(cryptoFile, progressAware), //
|
|
||||||
replace, //
|
|
||||||
encryptedTmpFile.length()), //
|
|
||||||
cryptoFile.getSize());
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
encryptedTmpFile.delete();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFile targetFile(CryptoFile cryptoFile, CloudFile cloudFile, boolean replace) throws BackendException {
|
|
||||||
if (replace || !cloudContentRepository.exists(cloudFile)) {
|
|
||||||
return cloudFile;
|
|
||||||
}
|
|
||||||
return firstNonExistingAutoRenamedFile(cryptoFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFile firstNonExistingAutoRenamedFile(CryptoFile original) throws BackendException {
|
|
||||||
String name = original.getName();
|
|
||||||
String nameWithoutExtension = nameWithoutExtension(name);
|
|
||||||
String extension = extension(name);
|
|
||||||
|
|
||||||
if (!extension.isEmpty()) {
|
|
||||||
extension = "." + extension;
|
|
||||||
}
|
|
||||||
|
|
||||||
int counter = 1;
|
|
||||||
CryptoFile result;
|
|
||||||
CloudFile cloudFile;
|
|
||||||
do {
|
|
||||||
String newFileName = nameWithoutExtension + " (" + counter + ")" + extension;
|
|
||||||
result = file(original.getParent(), newFileName, original.getSize());
|
|
||||||
counter++;
|
|
||||||
|
|
||||||
CloudFolder dirFolder = cloudContentRepository.folder(result.getCloudFile().getParent(), result.getCloudFile().getName());
|
|
||||||
cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, result.getSize());
|
|
||||||
} while (cloudContentRepository.exists(cloudFile));
|
|
||||||
return cloudFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertCryptoLongDirFileAlreadyExists(CloudFolder cryptoFolder) throws BackendException {
|
|
||||||
if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT))) {
|
|
||||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,538 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.common.io.BaseEncoding
|
||||||
|
import org.cryptomator.cryptolib.api.AuthenticationFailedException
|
||||||
|
import org.cryptomator.cryptolib.api.Cryptor
|
||||||
|
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel
|
||||||
|
import org.cryptomator.cryptolib.common.MessageDigestSupplier
|
||||||
|
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
|
||||||
|
import org.cryptomator.domain.exception.EmptyDirFileException
|
||||||
|
import org.cryptomator.domain.exception.FatalBackendException
|
||||||
|
import org.cryptomator.domain.exception.NoDirFileException
|
||||||
|
import org.cryptomator.domain.exception.NoSuchCloudFileException
|
||||||
|
import org.cryptomator.domain.exception.ParentFolderIsNullException
|
||||||
|
import org.cryptomator.domain.exception.SymLinkException
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource
|
||||||
|
import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from
|
||||||
|
import org.cryptomator.domain.usecases.cloud.Progress
|
||||||
|
import org.cryptomator.domain.usecases.cloud.UploadState
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.Channels
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.function.Supplier
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
import kotlin.streams.toList
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
open class CryptoImplVaultFormat7 : CryptoImplDecorator {
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
cryptor: Supplier<Cryptor>,
|
||||||
|
cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>,
|
||||||
|
storageLocation: CloudFolder,
|
||||||
|
dirIdCache: DirIdCache
|
||||||
|
) : super(
|
||||||
|
context, cryptor, cloudContentRepository, storageLocation, dirIdCache, CryptoConstants.DEFAULT_MAX_FILE_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
cryptor: Supplier<Cryptor>,
|
||||||
|
cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>,
|
||||||
|
storageLocation: CloudFolder,
|
||||||
|
dirIdCache: DirIdCache,
|
||||||
|
shorteningThreshold: Int
|
||||||
|
) : super(
|
||||||
|
context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold
|
||||||
|
)
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder {
|
||||||
|
val dirFileName = encryptFolderName(cryptoParent, cleartextName)
|
||||||
|
val dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).cloudFolder, dirFileName)
|
||||||
|
val dirFile = cloudContentRepository.file(dirFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT)
|
||||||
|
return folder(cryptoParent, cleartextName, dirFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun encryptName(cryptoParent: CryptoFolder, name: String): String {
|
||||||
|
var ciphertextName: String = cryptor() //
|
||||||
|
.fileNameCryptor() //
|
||||||
|
.encryptFilename(BaseEncoding.base64Url(), name, dirIdInfo(cryptoParent).id.toByteArray(StandardCharsets.UTF_8)) + CLOUD_NODE_EXT
|
||||||
|
if (ciphertextName.length > shorteningThreshold) {
|
||||||
|
ciphertextName = deflate(cryptoParent, ciphertextName)
|
||||||
|
}
|
||||||
|
return ciphertextName
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun deflate(cryptoParent: CryptoFolder, longFileName: String): String {
|
||||||
|
val longFilenameBytes = longFileName.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes)
|
||||||
|
val shortFileName = BaseEncoding.base64Url().encode(hash) + LONG_NODE_FILE_EXT
|
||||||
|
var dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).cloudFolder, shortFileName)
|
||||||
|
|
||||||
|
// if folder already exists in case of renaming
|
||||||
|
if (!cloudContentRepository.exists(dirFolder)) {
|
||||||
|
dirFolder = cloudContentRepository.create(dirFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
val data = longFileName.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
val cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT, data.size.toLong())
|
||||||
|
cloudContentRepository.write(cloudFile, from(data), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, true, data.size.toLong())
|
||||||
|
return shortFileName
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun metadataFile(cloudNode: CloudNode): CloudFile {
|
||||||
|
val cloudFolder = when (cloudNode) {
|
||||||
|
is CloudFile -> {
|
||||||
|
cloudNode.parent
|
||||||
|
}
|
||||||
|
is CloudFolder -> {
|
||||||
|
cloudNode
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw IllegalStateException("Should be file or folder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cloudContentRepository.file(cloudFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun inflate(cloudNode: CloudNode): String {
|
||||||
|
val metadataFile = metadataFile(cloudNode)
|
||||||
|
val out = ByteArrayOutputStream()
|
||||||
|
cloudContentRepository.read(metadataFile, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
|
||||||
|
return String(out.toByteArray(), StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun decryptName(dirId: String, encryptedName: String): String? {
|
||||||
|
return extractEncryptedName(encryptedName)?.let {
|
||||||
|
return cryptor().fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), it, dirId.toByteArray(StandardCharsets.UTF_8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun list(cryptoFolder: CryptoFolder): List<CryptoNode> {
|
||||||
|
dirIdCache.evictSubFoldersOf(cryptoFolder)
|
||||||
|
|
||||||
|
val dirIdInfo = dirIdInfo(cryptoFolder)
|
||||||
|
val dirId = dirIdInfo(cryptoFolder).id
|
||||||
|
val lvl2Dir = dirIdInfo.cloudFolder
|
||||||
|
|
||||||
|
val ciphertextNodes: List<CloudNode> = try {
|
||||||
|
cloudContentRepository.list(lvl2Dir)
|
||||||
|
} catch (e: NoSuchCloudFileException) {
|
||||||
|
if (cryptoFolder is RootCryptoFolder) {
|
||||||
|
Timber.tag("CryptoFs").e("No lvl2Dir exists for root folder in %s", lvl2Dir.path)
|
||||||
|
throw FatalBackendException(String.format("No lvl2Dir exists for root folder in %s", lvl2Dir.path), e)
|
||||||
|
} else if (cryptoFolder.dirFile == null) {
|
||||||
|
Timber.tag("CryptoFs").e(String.format("Dir-file of folder is null %s", lvl2Dir.path))
|
||||||
|
throw FatalBackendException(String.format("Dir-file of folder is null %s", lvl2Dir.path))
|
||||||
|
} else if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder.dirFile.parent, CLOUD_NODE_SYMLINK_PRE + CLOUD_NODE_EXT))) {
|
||||||
|
throw SymLinkException()
|
||||||
|
} else if (!cloudContentRepository.exists(cryptoFolder.dirFile)) {
|
||||||
|
Timber.tag("CryptoFs").e("No dir file exists in %s", cryptoFolder.dirFile.path)
|
||||||
|
throw NoDirFileException(cryptoFolder.name, cryptoFolder.dirFile.path)
|
||||||
|
}
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ciphertextNodes
|
||||||
|
.parallelStream()
|
||||||
|
.map { node ->
|
||||||
|
ciphertextToCleartextNode(cryptoFolder, dirId, node)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
.filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun ciphertextToCleartextNode(cryptoFolder: CryptoFolder, dirId: String, cloudNode: CloudNode): CryptoNode? {
|
||||||
|
var ciphertextName = cloudNode.name
|
||||||
|
var longNameFolderDirFile: CloudFile? = null
|
||||||
|
var longNameFile: CloudFile? = null
|
||||||
|
|
||||||
|
if (ciphertextName.endsWith(CLOUD_NODE_EXT)) {
|
||||||
|
ciphertextName = nameWithoutExtension(ciphertextName)
|
||||||
|
} else if (ciphertextName.endsWith(LONG_NODE_FILE_EXT)) {
|
||||||
|
ciphertextName = (longNodeCiphertextName(cloudNode) ?: return null)
|
||||||
|
for (node in cloudContentRepository.list((cloudNode as CloudFolder))) {
|
||||||
|
when (node.name) {
|
||||||
|
LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT -> longNameFile = node as CloudFile
|
||||||
|
CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT -> longNameFolderDirFile = node as CloudFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val cleartextName = decryptName(dirId, ciphertextName)
|
||||||
|
if (cleartextName == null) {
|
||||||
|
Timber.tag("CryptoFs").w("Failed to parse cipher text name of: %s", cloudNode.path)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
cloudNodeFromName(cloudNode, cryptoFolder, cleartextName, longNameFile, longNameFolderDirFile)
|
||||||
|
} catch (e: AuthenticationFailedException) {
|
||||||
|
Timber.tag("CryptoFs").w(e, "File/Folder name authentication failed: %s", cloudNode.path)
|
||||||
|
null
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Timber.tag("CryptoFs").w(e, "Illegal ciphertext filename/folder: %s", cloudNode.path)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun cloudNodeFromName(cloudNode: CloudNode, cryptoFolder: CryptoFolder, cleartextName: String, longNameFile: CloudFile?, dirFile: CloudFile?): CryptoNode? {
|
||||||
|
if (cloudNode is CloudFile) {
|
||||||
|
val cleartextSize = cloudNode.size?.let {
|
||||||
|
val ciphertextSizeWithoutHeader = it - cryptor().fileHeaderCryptor().headerSize()
|
||||||
|
if (ciphertextSizeWithoutHeader >= 0) {
|
||||||
|
cryptor().fileContentCryptor().cleartextSize(ciphertextSizeWithoutHeader)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file(cryptoFolder, cleartextName, cloudNode, cleartextSize)
|
||||||
|
} else if (cloudNode is CloudFolder) {
|
||||||
|
return if (longNameFile != null) {
|
||||||
|
// long file
|
||||||
|
val cleartextSize = longNameFile.size?.let {
|
||||||
|
val ciphertextSizeWithoutHeader: Long = it - cryptor().fileHeaderCryptor().headerSize()
|
||||||
|
if (ciphertextSizeWithoutHeader >= 0) {
|
||||||
|
cryptor().fileContentCryptor().cleartextSize(ciphertextSizeWithoutHeader)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file(cryptoFolder, cleartextName, longNameFile, cleartextSize)
|
||||||
|
} else {
|
||||||
|
// folder
|
||||||
|
if (dirFile != null) {
|
||||||
|
folder(cryptoFolder, cleartextName, dirFile)
|
||||||
|
} else {
|
||||||
|
val constructedDirFile = cloudContentRepository.file(cloudNode, "dir$CLOUD_NODE_EXT")
|
||||||
|
folder(cryptoFolder, cleartextName, constructedDirFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun longNodeCiphertextName(cloudNode: CloudNode): String? {
|
||||||
|
return try {
|
||||||
|
val ciphertextName = inflate(cloudNode)
|
||||||
|
nameWithoutExtension(ciphertextName)
|
||||||
|
} catch (e: NoSuchCloudFileException) {
|
||||||
|
Timber.tag("CryptoFs").e("Missing %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.path)
|
||||||
|
null
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
Timber.tag("CryptoFs").e(e, "Failed to read %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.path)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun createDirIdInfo(folder: CryptoFolder): DirIdInfo {
|
||||||
|
val dirId = loadDirId(folder)
|
||||||
|
return dirIdCache.put(folder, createDirIdInfoFor(dirId))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun encryptFolderName(cryptoFolder: CryptoFolder, name: String): String {
|
||||||
|
return encryptName(cryptoFolder, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun symlink(cryptoParent: CryptoFolder, cleartextName: String, target: String): CryptoSymlink {
|
||||||
|
throw FatalBackendException("FOOOO") // FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class, EmptyDirFileException::class)
|
||||||
|
override fun loadDirId(folder: CryptoFolder): String {
|
||||||
|
var dirFile: CloudFile? = null
|
||||||
|
if (folder.dirFile != null) {
|
||||||
|
dirFile = folder.dirFile
|
||||||
|
}
|
||||||
|
return if (RootCryptoFolder.isRoot(folder)) {
|
||||||
|
CryptoConstants.ROOT_DIR_ID
|
||||||
|
} else if (dirFile != null && cloudContentRepository.exists(dirFile)) {
|
||||||
|
String(loadContentsOfDirFile(dirFile), StandardCharsets.UTF_8)
|
||||||
|
} else {
|
||||||
|
newDirId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class, EmptyDirFileException::class)
|
||||||
|
private fun loadContentsOfDirFile(file: CloudFile): ByteArray {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream().use { out ->
|
||||||
|
cloudContentRepository.read(file, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
|
||||||
|
if (dirfileIsEmpty(out)) {
|
||||||
|
throw EmptyDirFileException(file.parent.name, file.path)
|
||||||
|
}
|
||||||
|
return out.toByteArray()
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun create(folder: CryptoFolder): CryptoFolder {
|
||||||
|
requireNotNull(folder.dirFile)
|
||||||
|
var shortName = false
|
||||||
|
if (folder.dirFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) {
|
||||||
|
assertCryptoLongDirFileAlreadyExists(folder)
|
||||||
|
} else {
|
||||||
|
assertCryptoFolderAlreadyExists(folder)
|
||||||
|
shortName = true
|
||||||
|
}
|
||||||
|
val dirIdInfo = dirIdInfo(folder)
|
||||||
|
val createdCloudFolder = cloudContentRepository.create(dirIdInfo.cloudFolder)
|
||||||
|
var dirFolder = folder.dirFile.parent
|
||||||
|
var dirFile = folder.dirFile
|
||||||
|
if (shortName) {
|
||||||
|
dirFolder = cloudContentRepository.create(dirFolder)
|
||||||
|
dirFile = cloudContentRepository.file(dirFolder, folder.dirFile.name)
|
||||||
|
}
|
||||||
|
val dirId = dirIdInfo.id.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
val createdDirFile = cloudContentRepository.write(dirFile, from(dirId), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, dirId.size.toLong())
|
||||||
|
val result = folder(folder, createdDirFile)
|
||||||
|
addFolderToCache(result, dirIdInfo.withCloudFolder(createdCloudFolder))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun extractEncryptedName(ciphertextName: String): String? {
|
||||||
|
val matcher = BASE64_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName)
|
||||||
|
return if (matcher.find(0)) {
|
||||||
|
matcher.group()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder {
|
||||||
|
requireNotNull(source.dirFile)
|
||||||
|
requireNotNull(target.dirFile)
|
||||||
|
target.parent?.let { targetsParent ->
|
||||||
|
var shortName = false
|
||||||
|
if (target.dirFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) {
|
||||||
|
assertCryptoLongDirFileAlreadyExists(target)
|
||||||
|
} else {
|
||||||
|
assertCryptoFolderAlreadyExists(target)
|
||||||
|
shortName = true
|
||||||
|
}
|
||||||
|
var targetDirFile = target.dirFile
|
||||||
|
if (shortName) {
|
||||||
|
val targetDirFolder = cloudContentRepository.create(target.dirFile.parent)
|
||||||
|
targetDirFile = cloudContentRepository.file(targetDirFolder, target.dirFile.name)
|
||||||
|
}
|
||||||
|
val result = folder(targetsParent, target.name, cloudContentRepository.move(source.dirFile, targetDirFile))
|
||||||
|
cloudContentRepository.delete(source.dirFile.parent)
|
||||||
|
evictFromCache(source)
|
||||||
|
evictFromCache(target)
|
||||||
|
return result
|
||||||
|
} ?: throw ParentFolderIsNullException(target.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: CryptoFile, target: CryptoFile): CryptoFile {
|
||||||
|
return if (source.cloudFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) {
|
||||||
|
val targetDirFolder = cloudContentRepository.folder(target.cloudFile.parent, target.cloudFile.name)
|
||||||
|
val cryptoFile: CryptoFile = if (target.cloudFile.name.endsWith(LONG_NODE_FILE_EXT)) {
|
||||||
|
assertCryptoLongDirFileAlreadyExists(targetDirFolder)
|
||||||
|
moveLongFileToLongFile(source, target, targetDirFolder)
|
||||||
|
} else {
|
||||||
|
assertCryptoFileAlreadyExists(target)
|
||||||
|
moveLongFileToShortFile(source, target)
|
||||||
|
}
|
||||||
|
source.cloudFile.parent.parent?.let {
|
||||||
|
val sourceDirFolder = cloudContentRepository.folder(it, source.cloudFile.parent.name)
|
||||||
|
cloudContentRepository.delete(sourceDirFolder)
|
||||||
|
}
|
||||||
|
cryptoFile
|
||||||
|
} else {
|
||||||
|
if (target.cloudFile.name.endsWith(LONG_NODE_FILE_EXT)) {
|
||||||
|
val targetDirFolder = cloudContentRepository.folder(target.cloudFile.parent, target.cloudFile.name)
|
||||||
|
assertCryptoLongDirFileAlreadyExists(targetDirFolder)
|
||||||
|
moveShortFileToLongFile(source, target, targetDirFolder)
|
||||||
|
} else {
|
||||||
|
assertCryptoFileAlreadyExists(target)
|
||||||
|
file(target, cloudContentRepository.move(source.cloudFile, target.cloudFile), source.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun moveLongFileToLongFile(source: CryptoFile, target: CryptoFile, targetDirFolder: CloudFolder): CryptoFile {
|
||||||
|
requireNotNull(source.cloudFile.parent)
|
||||||
|
val sourceFile = cloudContentRepository.file(source.cloudFile.parent, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT)
|
||||||
|
val movedFile = cloudContentRepository.move(sourceFile, cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT))
|
||||||
|
return file(target, movedFile, movedFile.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun moveLongFileToShortFile(source: CryptoFile, target: CryptoFile): CryptoFile {
|
||||||
|
requireNotNull(source.cloudFile.parent)
|
||||||
|
val sourceFile = cloudContentRepository.file(source.cloudFile.parent, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT)
|
||||||
|
val movedFile = cloudContentRepository.move(sourceFile, target.cloudFile)
|
||||||
|
return file(target, movedFile, movedFile.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun moveShortFileToLongFile(source: CryptoFile, target: CryptoFile, targetDirFolder: CloudFolder): CryptoFile {
|
||||||
|
val movedFile = cloudContentRepository.move(source.cloudFile, cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT))
|
||||||
|
return file(target, movedFile, movedFile.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun delete(node: CloudNode) {
|
||||||
|
if (node is CryptoFolder) {
|
||||||
|
requireNotNull(node.dirFile)
|
||||||
|
val cryptoSubfolders = deepCollectSubfolders(node)
|
||||||
|
for (cryptoSubfolder in cryptoSubfolders) {
|
||||||
|
try {
|
||||||
|
cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).cloudFolder)
|
||||||
|
} catch (e: NoSuchCloudFileException) {
|
||||||
|
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
cloudContentRepository.delete(dirIdInfo(node).cloudFolder)
|
||||||
|
} catch (e: NoSuchCloudFileException) {
|
||||||
|
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
|
||||||
|
}
|
||||||
|
cloudContentRepository.delete(node.dirFile.parent)
|
||||||
|
evictFromCache(node)
|
||||||
|
} else if (node is CryptoFile) {
|
||||||
|
if (node.cloudFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) {
|
||||||
|
cloudContentRepository.delete(node.cloudFile.parent)
|
||||||
|
} else {
|
||||||
|
cloudContentRepository.delete(node.cloudFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun write(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile {
|
||||||
|
return if (cryptoFile.cloudFile.name.endsWith(LONG_NODE_FILE_EXT)) {
|
||||||
|
writeLongFile(cryptoFile, data, progressAware, replace, length)
|
||||||
|
} else {
|
||||||
|
writeShortNameFile(cryptoFile, data, progressAware, replace, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun writeLongFile(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile {
|
||||||
|
val dirFolder = cloudContentRepository.folder(cryptoFile.cloudFile.parent, cryptoFile.cloudFile.name)
|
||||||
|
val cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, data.size(context))
|
||||||
|
assertCryptoLongDirFileAlreadyExists(dirFolder)
|
||||||
|
try {
|
||||||
|
data.open(context)?.use { stream ->
|
||||||
|
val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache)
|
||||||
|
try {
|
||||||
|
Channels.newChannel(FileOutputStream(encryptedTmpFile)).use { writableByteChannel ->
|
||||||
|
EncryptingWritableByteChannel(writableByteChannel, cryptor()).use { encryptingWritableByteChannel ->
|
||||||
|
cloudFile.size?.let { size ->
|
||||||
|
progressAware.onProgress(Progress.started(UploadState.encryption(cloudFile)))
|
||||||
|
val buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize())
|
||||||
|
val ciphertextSize = cryptor().fileContentCryptor().ciphertextSize(size) + cryptor().fileHeaderCryptor().headerSize()
|
||||||
|
var read: Int
|
||||||
|
var encrypted: Long = 0
|
||||||
|
while (stream.read(buff.array()).also { read = it } > 0) {
|
||||||
|
buff.limit(read)
|
||||||
|
val written = encryptingWritableByteChannel.write(buff)
|
||||||
|
buff.flip()
|
||||||
|
encrypted += written.toLong()
|
||||||
|
progressAware.onProgress(Progress.progress(UploadState.encryption(cloudFile)).between(0).and(ciphertextSize).withValue(encrypted))
|
||||||
|
}
|
||||||
|
encryptingWritableByteChannel.close()
|
||||||
|
progressAware.onProgress(Progress.completed(UploadState.encryption(cloudFile)))
|
||||||
|
val targetFile = targetFile(cryptoFile, cloudFile, replace)
|
||||||
|
return file(
|
||||||
|
cryptoFile, //
|
||||||
|
cloudContentRepository.write( //
|
||||||
|
targetFile, //
|
||||||
|
data.decorate(from(encryptedTmpFile)),
|
||||||
|
UploadFileReplacingProgressAware(cryptoFile, progressAware), //
|
||||||
|
replace, //
|
||||||
|
encryptedTmpFile.length()
|
||||||
|
), //
|
||||||
|
cryptoFile.size
|
||||||
|
)
|
||||||
|
} ?: throw FatalBackendException("CloudFile size shouldn't be null")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
encryptedTmpFile.delete()
|
||||||
|
}
|
||||||
|
} ?: throw FatalBackendException("InputStream shouldn't be null")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun targetFile(cryptoFile: CryptoFile, cloudFile: CloudFile, replace: Boolean): CloudFile {
|
||||||
|
return if (replace || !cloudContentRepository.exists(cloudFile)) {
|
||||||
|
cloudFile
|
||||||
|
} else firstNonExistingAutoRenamedFile(cryptoFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun firstNonExistingAutoRenamedFile(original: CryptoFile): CloudFile {
|
||||||
|
val name = original.name
|
||||||
|
val nameWithoutExtension = nameWithoutExtension(name)
|
||||||
|
var extension = extension(name)
|
||||||
|
if (extension.isNotEmpty()) {
|
||||||
|
extension = ".$extension"
|
||||||
|
}
|
||||||
|
var counter = 1
|
||||||
|
var result: CryptoFile
|
||||||
|
var cloudFile: CloudFile
|
||||||
|
do {
|
||||||
|
val newFileName = "$nameWithoutExtension ($counter)$extension"
|
||||||
|
result = file(original.parent, newFileName, original.size)
|
||||||
|
counter++
|
||||||
|
val dirFolder = cloudContentRepository.folder(result.cloudFile.parent, result.cloudFile.name)
|
||||||
|
cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, result.size)
|
||||||
|
} while (cloudContentRepository.exists(cloudFile))
|
||||||
|
return cloudFile
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun assertCryptoLongDirFileAlreadyExists(cryptoFolder: CloudFolder) {
|
||||||
|
if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT))) {
|
||||||
|
throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val CLOUD_NODE_EXT = ".c9r"
|
||||||
|
private const val LONG_NODE_FILE_EXT = ".c9s"
|
||||||
|
private const val CLOUD_FOLDER_DIR_FILE_PRE = "dir"
|
||||||
|
private const val LONG_NODE_FILE_CONTENT_CONTENTS = "contents"
|
||||||
|
private const val LONG_NODE_FILE_CONTENT_NAME = "name"
|
||||||
|
private const val CLOUD_NODE_SYMLINK_PRE = "symlink"
|
||||||
|
private val BASE64_ENCRYPTED_NAME_PATTERN = Pattern.compile("^([A-Za-z0-9+/\\-_]{4})*([A-Za-z0-9+/\\-]{4}|[A-Za-z0-9+/\\-_]{3}=|[A-Za-z0-9+/\\-_]{2}==)?$")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.cryptomator.cryptolib.api.Cryptor
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
|
class CryptoImplVaultFormat8 internal constructor(
|
||||||
|
context: Context,
|
||||||
|
cryptor: Supplier<Cryptor>,
|
||||||
|
cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>,
|
||||||
|
storageLocation: CloudFolder,
|
||||||
|
dirIdCache: DirIdCache,
|
||||||
|
shorteningThreshold: Int
|
||||||
|
) : CryptoImplVaultFormat7(
|
||||||
|
context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold
|
||||||
|
)
|
@ -1,270 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Base32;
|
|
||||||
import org.apache.commons.codec.binary.BaseNCodec;
|
|
||||||
import org.cryptomator.cryptolib.Cryptors;
|
|
||||||
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
|
|
||||||
import org.cryptomator.cryptolib.api.Cryptor;
|
|
||||||
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
import org.cryptomator.domain.exception.AlreadyExistException;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
import org.cryptomator.domain.exception.EmptyDirFileException;
|
|
||||||
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
import org.cryptomator.util.Supplier;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
|
||||||
import static org.cryptomator.util.Encodings.UTF_8;
|
|
||||||
|
|
||||||
final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator {
|
|
||||||
|
|
||||||
private static final int SHORT_NAMES_MAX_LENGTH = 129;
|
|
||||||
private static final String DIR_PREFIX = "0";
|
|
||||||
private static final String SYMLINK_PREFIX = "1S";
|
|
||||||
private static final String LONG_NAME_FILE_EXT = ".lng";
|
|
||||||
private static final String METADATA_DIR_NAME = "m";
|
|
||||||
|
|
||||||
private static final BaseNCodec BASE32 = new Base32();
|
|
||||||
private static final Pattern BASE32_ENCRYPTED_NAME_PATTERN = Pattern.compile("^(0|1S)?(([A-Z2-7]{8})*[A-Z2-7=]{8})$");
|
|
||||||
|
|
||||||
CryptoImplVaultFormatPre7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
|
|
||||||
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException {
|
|
||||||
String dirFileName = encryptFolderName(cryptoParent, cleartextName);
|
|
||||||
CloudFile dirFile = cloudContentRepository.file(dirIdInfo(cryptoParent).getCloudFolder(), dirFileName);
|
|
||||||
return folder(cryptoParent, cleartextName, dirFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CryptoFolder create(CryptoFolder folder) throws BackendException {
|
|
||||||
assertCryptoFolderAlreadyExists(folder);
|
|
||||||
DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(folder);
|
|
||||||
CloudFolder createdCloudFolder = cloudContentRepository.create(dirIdInfo.getCloudFolder());
|
|
||||||
byte[] dirId = dirIdInfo.getId().getBytes(UTF_8);
|
|
||||||
CloudFile createdDirFile = cloudContentRepository.write(folder.getDirFile(), ByteArrayDataSource.from(dirId), NO_OP_PROGRESS_AWARE, false, dirId.length);
|
|
||||||
CryptoFolder result = folder(folder, createdDirFile);
|
|
||||||
addFolderToCache(result, dirIdInfo.withCloudFolder(createdCloudFolder));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String encryptName(CryptoFolder cryptoParent, String name) throws BackendException {
|
|
||||||
return encryptName(cryptoParent, name, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String encryptName(CryptoFolder cryptoParent, String name, String prefix) throws BackendException {
|
|
||||||
String ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(name, dirIdInfo(cryptoParent).getId().getBytes(UTF_8));
|
|
||||||
if (ciphertextName.length() > SHORT_NAMES_MAX_LENGTH) {
|
|
||||||
ciphertextName = deflate(ciphertextName);
|
|
||||||
}
|
|
||||||
return ciphertextName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String deflate(String longFileName) throws BackendException {
|
|
||||||
byte[] longFilenameBytes = longFileName.getBytes(UTF_8);
|
|
||||||
byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes);
|
|
||||||
String shortFileName = BASE32.encodeAsString(hash) + LONG_NAME_FILE_EXT;
|
|
||||||
CloudFile metadataFile = metadataFile(shortFileName);
|
|
||||||
byte[] data = longFileName.getBytes(UTF_8);
|
|
||||||
try {
|
|
||||||
cloudContentRepository.create(metadataFile.getParent());
|
|
||||||
} catch (AlreadyExistException e) {
|
|
||||||
}
|
|
||||||
cloudContentRepository.write(metadataFile, ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, true, data.length);
|
|
||||||
return shortFileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String inflate(String shortFileName) throws BackendException {
|
|
||||||
CloudFile metadataFile = metadataFile(shortFileName);
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
||||||
cloudContentRepository.read(metadataFile, Optional.empty(), out, NO_OP_PROGRESS_AWARE);
|
|
||||||
return new String(out.toByteArray(), UTF_8);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFile inflatePermanently(CloudFile cloudFile, String longFileName) throws BackendException {
|
|
||||||
Timber.tag("CryptoFs").i("inflatePermanently: %s -> %s", cloudFile.getName(), longFileName);
|
|
||||||
CloudFile newCiphertextFile = cloudContentRepository.file(cloudFile.getParent(), longFileName);
|
|
||||||
cloudContentRepository.move(cloudFile, newCiphertextFile);
|
|
||||||
return newCiphertextFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFile metadataFile(String shortFilename) throws BackendException {
|
|
||||||
CloudFolder firstLevelFolder = cloudContentRepository.folder(metadataFolder(), shortFilename.substring(0, 2));
|
|
||||||
CloudFolder secondLevelFolder = cloudContentRepository.folder(firstLevelFolder, shortFilename.substring(2, 4));
|
|
||||||
return cloudContentRepository.file(secondLevelFolder, shortFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFolder metadataFolder() throws BackendException {
|
|
||||||
return cloudContentRepository.folder(storageLocation(), METADATA_DIR_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
List<CryptoNode> list(CryptoFolder cryptoFolder) throws BackendException {
|
|
||||||
DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(cryptoFolder);
|
|
||||||
String dirId = dirIdInfo(cryptoFolder).getId();
|
|
||||||
CloudFolder lvl2Dir = dirIdInfo.getCloudFolder();
|
|
||||||
List<CloudNode> ciphertextNodes = cloudContentRepository.list(lvl2Dir);
|
|
||||||
List<CryptoNode> result = new ArrayList<>();
|
|
||||||
for (CloudNode node : ciphertextNodes) {
|
|
||||||
if (node instanceof CloudFile) {
|
|
||||||
ciphertextToCleartextNode(cryptoFolder, dirId, node).ifPresent(result::add);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<CryptoNode> ciphertextToCleartextNode(CryptoFolder cryptoFolder, String dirId, CloudNode cloudNode) throws BackendException {
|
|
||||||
CloudFile cloudFile = (CloudFile) cloudNode;
|
|
||||||
String ciphertextName = cloudFile.getName();
|
|
||||||
if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) {
|
|
||||||
try {
|
|
||||||
ciphertextName = inflate(ciphertextName);
|
|
||||||
if (ciphertextName.length() <= SHORT_NAMES_MAX_LENGTH) {
|
|
||||||
cloudFile = inflatePermanently(cloudFile, ciphertextName);
|
|
||||||
}
|
|
||||||
} catch (NoSuchCloudFileException e) {
|
|
||||||
Timber.tag("CryptoFs").e("Missing mFile: %s", ciphertextName);
|
|
||||||
return Optional.empty();
|
|
||||||
} catch (BackendException e) {
|
|
||||||
Timber.tag("CryptoFs").e(e, "Failed to read mFile: %s", ciphertextName);
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String cleartextName;
|
|
||||||
try {
|
|
||||||
cleartextName = decryptName(dirId, ciphertextName.toUpperCase());
|
|
||||||
} catch (AuthenticationFailedException e) {
|
|
||||||
Timber.tag("CryptoFs").w("File name authentication failed: %s", cloudFile.getPath());
|
|
||||||
return Optional.empty();
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
Timber.tag("CryptoFs").d("Illegal ciphertext filename: %s", cloudFile.getPath());
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
if (cleartextName == null || ciphertextName.startsWith(SYMLINK_PREFIX)) {
|
|
||||||
return Optional.empty();
|
|
||||||
} else if (ciphertextName.startsWith(DIR_PREFIX)) {
|
|
||||||
return Optional.of(folder(cryptoFolder, cleartextName, cloudFile));
|
|
||||||
} else {
|
|
||||||
Optional<Long> cleartextSize = Optional.empty();
|
|
||||||
if (cloudFile.getSize().isPresent()) {
|
|
||||||
long ciphertextSizeWithoutHeader = cloudFile.getSize().get() - cryptor().fileHeaderCryptor().headerSize();
|
|
||||||
if (ciphertextSizeWithoutHeader >= 0) {
|
|
||||||
cleartextSize = Optional.of(Cryptors.cleartextSize(ciphertextSizeWithoutHeader, cryptor()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.of(file(cryptoFolder, cleartextName, cloudFile, cleartextSize));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String decryptName(String dirId, String encryptedName) {
|
|
||||||
Optional<String> ciphertextName = extractEncryptedName(encryptedName);
|
|
||||||
if (ciphertextName.isPresent()) {
|
|
||||||
return cryptor().fileNameCryptor().decryptFilename(ciphertextName.get(), dirId.getBytes(UTF_8));
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Optional<String> extractEncryptedName(String ciphertextName) {
|
|
||||||
Matcher matcher = BASE32_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName);
|
|
||||||
if (matcher.find(0)) {
|
|
||||||
return Optional.of(matcher.group(2));
|
|
||||||
} else {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CryptoSymlink symlink(CryptoFolder cryptoParent, String cleartextName, String target) throws BackendException {
|
|
||||||
String ciphertextName = encryptSymlinkName(cryptoParent, cleartextName);
|
|
||||||
CloudFile cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).getCloudFolder(), ciphertextName);
|
|
||||||
return new CryptoSymlink(cryptoParent, cleartextName, path(cryptoParent, cleartextName), target, cloudFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String encryptSymlinkName(CryptoFolder cryptoFolder, String name) throws BackendException {
|
|
||||||
return encryptName(cryptoFolder, name, SYMLINK_PREFIX);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String encryptFolderName(CryptoFolder cryptoFolder, String name) throws BackendException {
|
|
||||||
return encryptName(cryptoFolder, name, DIR_PREFIX);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException {
|
|
||||||
assertCryptoFolderAlreadyExists(target);
|
|
||||||
CryptoFolder result = folder(target.getParent(), target.getName(), cloudContentRepository.move(source.getDirFile(), target.getDirFile()));
|
|
||||||
|
|
||||||
evictFromCache(source);
|
|
||||||
evictFromCache(target);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException {
|
|
||||||
assertCryptoFileAlreadyExists(target);
|
|
||||||
return file(target, cloudContentRepository.move(source.getCloudFile(), target.getCloudFile()), source.getSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void delete(CloudNode node) throws BackendException {
|
|
||||||
if (node instanceof CryptoFolder) {
|
|
||||||
CryptoFolder cryptoFolder = (CryptoFolder) node;
|
|
||||||
List<CryptoFolder> cryptoSubfolders = deepCollectSubfolders(cryptoFolder);
|
|
||||||
for (CryptoFolder cryptoSubfolder : cryptoSubfolders) {
|
|
||||||
cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).getCloudFolder());
|
|
||||||
}
|
|
||||||
cloudContentRepository.delete(dirIdInfo(cryptoFolder).getCloudFolder());
|
|
||||||
cloudContentRepository.delete(cryptoFolder.getDirFile());
|
|
||||||
evictFromCache(cryptoFolder);
|
|
||||||
} else if (node instanceof CryptoFile) {
|
|
||||||
CryptoFile cryptoFile = (CryptoFile) node;
|
|
||||||
cloudContentRepository.delete(cryptoFile.getCloudFile());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String loadDirId(CryptoFolder folder) throws BackendException, EmptyDirFileException {
|
|
||||||
if (RootCryptoFolder.isRoot(folder)) {
|
|
||||||
return CryptoConstants.ROOT_DIR_ID;
|
|
||||||
} else if (cloudContentRepository.exists(folder.getDirFile())) {
|
|
||||||
return new String(loadContentsOfDirFile(folder), UTF_8);
|
|
||||||
} else {
|
|
||||||
return newDirId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
DirIdCache.DirIdInfo createDirIdInfo(CryptoFolder folder) throws BackendException {
|
|
||||||
String dirId = loadDirId(folder);
|
|
||||||
return dirIdCache.put(folder, createDirIdInfoFor(dirId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFile write(CryptoFile cryptoFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException {
|
|
||||||
return writeShortNameFile(cryptoFile, data, progressAware, replace, length);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,283 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.common.io.BaseEncoding
|
||||||
|
import org.apache.commons.codec.binary.Base32
|
||||||
|
import org.apache.commons.codec.binary.BaseNCodec
|
||||||
|
import org.cryptomator.cryptolib.api.AuthenticationFailedException
|
||||||
|
import org.cryptomator.cryptolib.api.Cryptor
|
||||||
|
import org.cryptomator.cryptolib.common.MessageDigestSupplier
|
||||||
|
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo
|
||||||
|
import org.cryptomator.data.cloud.crypto.RootCryptoFolder.Companion.isRoot
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
import org.cryptomator.domain.exception.AlreadyExistException
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
import org.cryptomator.domain.exception.EmptyDirFileException
|
||||||
|
import org.cryptomator.domain.exception.NoSuchCloudFileException
|
||||||
|
import org.cryptomator.domain.exception.ParentFolderIsNullException
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource
|
||||||
|
import org.cryptomator.domain.usecases.cloud.UploadState
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.function.Supplier
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
import kotlin.streams.toList
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
internal class CryptoImplVaultFormatPre7(
|
||||||
|
context: Context,
|
||||||
|
cryptor: Supplier<Cryptor>,
|
||||||
|
cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>,
|
||||||
|
storageLocation: CloudFolder,
|
||||||
|
dirIdCache: DirIdCache
|
||||||
|
) :
|
||||||
|
CryptoImplDecorator(
|
||||||
|
context, cryptor, cloudContentRepository, storageLocation, dirIdCache, SHORTENING_THRESHOLD
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder {
|
||||||
|
val dirFileName = encryptFolderName(cryptoParent, cleartextName)
|
||||||
|
val dirFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, dirFileName)
|
||||||
|
return folder(cryptoParent, cleartextName, dirFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun create(folder: CryptoFolder): CryptoFolder {
|
||||||
|
requireNotNull(folder.dirFile)
|
||||||
|
assertCryptoFolderAlreadyExists(folder)
|
||||||
|
val dirIdInfo = dirIdInfo(folder)
|
||||||
|
val createdCloudFolder = cloudContentRepository.create(dirIdInfo.cloudFolder)
|
||||||
|
val dirId = dirIdInfo.id.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
val createdDirFile = cloudContentRepository.write(folder.dirFile, from(dirId), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, dirId.size.toLong())
|
||||||
|
return folder(folder, createdDirFile).also {
|
||||||
|
addFolderToCache(it, dirIdInfo.withCloudFolder(createdCloudFolder))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun encryptName(cryptoParent: CryptoFolder, name: String): String {
|
||||||
|
return encryptName(cryptoParent, name, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun encryptName(cryptoParent: CryptoFolder, name: String, prefix: String): String {
|
||||||
|
var ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(BaseEncoding.base32(), name, dirIdInfo(cryptoParent).id.toByteArray(StandardCharsets.UTF_8))
|
||||||
|
if (ciphertextName.length > shorteningThreshold) {
|
||||||
|
ciphertextName = deflate(ciphertextName)
|
||||||
|
}
|
||||||
|
return ciphertextName
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun deflate(longFileName: String): String {
|
||||||
|
val longFilenameBytes = longFileName.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes)
|
||||||
|
val shortFileName = BASE32.encodeAsString(hash) + LONG_NAME_FILE_EXT
|
||||||
|
val metadataFile = metadataFile(shortFileName)
|
||||||
|
val data = longFileName.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
try {
|
||||||
|
cloudContentRepository.create(metadataFile.parent)
|
||||||
|
} catch (e: AlreadyExistException) {
|
||||||
|
}
|
||||||
|
cloudContentRepository.write(metadataFile, from(data), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, true, data.size.toLong())
|
||||||
|
return shortFileName
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun inflate(shortFileName: String): String {
|
||||||
|
val metadataFile = metadataFile(shortFileName)
|
||||||
|
val out = ByteArrayOutputStream()
|
||||||
|
cloudContentRepository.read(metadataFile, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
|
||||||
|
return String(out.toByteArray(), StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun inflatePermanently(cloudFile: CloudFile, longFileName: String): CloudFile {
|
||||||
|
Timber.tag("CryptoFs").i("inflatePermanently: %s -> %s", cloudFile.name, longFileName)
|
||||||
|
val newCiphertextFile = cloudContentRepository.file(cloudFile.parent, longFileName)
|
||||||
|
cloudContentRepository.move(cloudFile, newCiphertextFile)
|
||||||
|
return newCiphertextFile
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun metadataFile(shortFilename: String): CloudFile {
|
||||||
|
val firstLevelFolder = cloudContentRepository.folder(metadataFolder(), shortFilename.substring(0, 2))
|
||||||
|
val secondLevelFolder = cloudContentRepository.folder(firstLevelFolder, shortFilename.substring(2, 4))
|
||||||
|
return cloudContentRepository.file(secondLevelFolder, shortFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun metadataFolder(): CloudFolder {
|
||||||
|
return cloudContentRepository.folder(storageLocation(), METADATA_DIR_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun list(cryptoFolder: CryptoFolder): List<CryptoNode> {
|
||||||
|
val dirIdInfo = dirIdInfo(cryptoFolder)
|
||||||
|
val dirId = dirIdInfo(cryptoFolder).id
|
||||||
|
val lvl2Dir = dirIdInfo.cloudFolder
|
||||||
|
return cloudContentRepository
|
||||||
|
.list(lvl2Dir)
|
||||||
|
.filterIsInstance<CloudFile>()
|
||||||
|
.parallelStream()
|
||||||
|
.map { node ->
|
||||||
|
ciphertextToCleartextNode(cryptoFolder, dirId, node)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
.filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun ciphertextToCleartextNode(cryptoFolder: CryptoFolder, dirId: String, cloudNode: CloudFile): CryptoNode? {
|
||||||
|
var cloudFile = cloudNode
|
||||||
|
var ciphertextName = cloudFile.name
|
||||||
|
if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) {
|
||||||
|
try {
|
||||||
|
ciphertextName = inflate(ciphertextName)
|
||||||
|
if (ciphertextName.length <= shorteningThreshold) {
|
||||||
|
cloudFile = inflatePermanently(cloudFile, ciphertextName)
|
||||||
|
}
|
||||||
|
} catch (e: NoSuchCloudFileException) {
|
||||||
|
Timber.tag("CryptoFs").e("Missing mFile: %s", ciphertextName)
|
||||||
|
return null
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
Timber.tag("CryptoFs").e(e, "Failed to read mFile: %s", ciphertextName)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val cleartextName: String? = try {
|
||||||
|
decryptName(dirId, ciphertextName.uppercase())
|
||||||
|
} catch (e: AuthenticationFailedException) {
|
||||||
|
Timber.tag("CryptoFs").w("File name authentication failed: %s", cloudFile.path)
|
||||||
|
return null
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Timber.tag("CryptoFs").d("Illegal ciphertext filename: %s", cloudFile.path)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return if (cleartextName == null || ciphertextName.startsWith(SYMLINK_PREFIX)) {
|
||||||
|
null
|
||||||
|
} else if (ciphertextName.startsWith(DIR_PREFIX)) {
|
||||||
|
folder(cryptoFolder, cleartextName, cloudFile)
|
||||||
|
} else {
|
||||||
|
val cleartextSize = cloudFile.size?.let {
|
||||||
|
val ciphertextSizeWithoutHeader: Long = it - cryptor().fileHeaderCryptor().headerSize()
|
||||||
|
if (ciphertextSizeWithoutHeader >= 0) {
|
||||||
|
cryptor().fileContentCryptor().cleartextSize(ciphertextSizeWithoutHeader)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file(cryptoFolder, cleartextName, cloudFile, cleartextSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun decryptName(dirId: String, encryptedName: String): String? {
|
||||||
|
val ciphertextName = extractEncryptedName(encryptedName)
|
||||||
|
return if (ciphertextName != null) {
|
||||||
|
cryptor().fileNameCryptor().decryptFilename(BaseEncoding.base32(), ciphertextName, dirId.toByteArray(StandardCharsets.UTF_8))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun extractEncryptedName(ciphertextName: String): String? {
|
||||||
|
val matcher = BASE32_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName)
|
||||||
|
return if (matcher.find(0)) {
|
||||||
|
matcher.group(2)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun symlink(cryptoParent: CryptoFolder, cleartextName: String, target: String): CryptoSymlink {
|
||||||
|
val ciphertextName = encryptSymlinkName(cryptoParent, cleartextName)
|
||||||
|
val cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, ciphertextName)
|
||||||
|
return CryptoSymlink(cryptoParent, cleartextName, path(cryptoParent, cleartextName), target, cloudFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun encryptSymlinkName(cryptoFolder: CryptoFolder, name: String): String {
|
||||||
|
return encryptName(cryptoFolder, name, SYMLINK_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun encryptFolderName(cryptoFolder: CryptoFolder, name: String): String {
|
||||||
|
return encryptName(cryptoFolder, name, DIR_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder {
|
||||||
|
requireNotNull(source.dirFile)
|
||||||
|
requireNotNull(target.dirFile)
|
||||||
|
target.parent?.let {
|
||||||
|
assertCryptoFolderAlreadyExists(target)
|
||||||
|
return folder(it, target.name, cloudContentRepository.move(source.dirFile, target.dirFile)).also {
|
||||||
|
evictFromCache(source)
|
||||||
|
evictFromCache(target)
|
||||||
|
}
|
||||||
|
} ?: throw ParentFolderIsNullException(target.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: CryptoFile, target: CryptoFile): CryptoFile {
|
||||||
|
assertCryptoFileAlreadyExists(target)
|
||||||
|
return file(target, cloudContentRepository.move(source.cloudFile, target.cloudFile), source.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun delete(node: CloudNode) {
|
||||||
|
if (node is CryptoFolder) {
|
||||||
|
requireNotNull(node.dirFile)
|
||||||
|
val cryptoSubfolders = deepCollectSubfolders(node)
|
||||||
|
for (cryptoSubfolder in cryptoSubfolders) {
|
||||||
|
cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).cloudFolder)
|
||||||
|
}
|
||||||
|
cloudContentRepository.delete(dirIdInfo(node).cloudFolder)
|
||||||
|
cloudContentRepository.delete(node.dirFile)
|
||||||
|
evictFromCache(node)
|
||||||
|
} else if (node is CryptoFile) {
|
||||||
|
cloudContentRepository.delete(node.cloudFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class, EmptyDirFileException::class)
|
||||||
|
override fun loadDirId(folder: CryptoFolder): String {
|
||||||
|
return if (isRoot(folder)) {
|
||||||
|
CryptoConstants.ROOT_DIR_ID
|
||||||
|
} else if (folder.dirFile != null && cloudContentRepository.exists(folder.dirFile)) {
|
||||||
|
String(loadContentsOfDirFile(folder), StandardCharsets.UTF_8)
|
||||||
|
} else {
|
||||||
|
newDirId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun createDirIdInfo(folder: CryptoFolder): DirIdInfo {
|
||||||
|
val dirId = loadDirId(folder)
|
||||||
|
return dirIdCache.put(folder, createDirIdInfoFor(dirId))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun write(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile {
|
||||||
|
return writeShortNameFile(cryptoFile, data, progressAware, replace, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val SHORTENING_THRESHOLD = 129
|
||||||
|
private const val DIR_PREFIX = "0"
|
||||||
|
private const val SYMLINK_PREFIX = "1S"
|
||||||
|
private const val LONG_NAME_FILE_EXT = ".lng"
|
||||||
|
private const val METADATA_DIR_NAME = "m"
|
||||||
|
private val BASE32: BaseNCodec = Base32()
|
||||||
|
private val BASE32_ENCRYPTED_NAME_PATTERN = Pattern.compile("^(0|1S)?(([A-Z2-7]{8})*[A-Z2-7=]{8})$")
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
|
|
||||||
interface CryptoNode extends CloudNode {
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,5 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
|
||||||
|
interface CryptoNode : CloudNode
|
@ -1,82 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
class CryptoSymlink implements CloudFile, CryptoNode {
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
private final String path;
|
|
||||||
private final String target;
|
|
||||||
private final CloudFile cloudFile;
|
|
||||||
private final CryptoFolder parent;
|
|
||||||
|
|
||||||
public CryptoSymlink(CryptoFolder parent, String name, String path, String target, CloudFile cloudFile) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
this.target = target;
|
|
||||||
this.cloudFile = cloudFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return parent.getCloud();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFolder getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Long> getSize() {
|
|
||||||
return Optional.of((long) target.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Date> getModified() {
|
|
||||||
return cloudFile.getModified();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The actual file in the underlying, i.e. decorated, CloudContentRepository
|
|
||||||
*/
|
|
||||||
CloudFile getCloudFile() {
|
|
||||||
return cloudFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (obj == null || getClass() != obj.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (obj == this) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return internalEquals((CryptoSymlink) obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean internalEquals(CryptoSymlink obj) {
|
|
||||||
return path != null && path.equals(obj.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return path == null ? 0 : path.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,40 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class CryptoSymlink(
|
||||||
|
override val parent: CryptoFolder, override val name: String, override val path: String, private val target: String,
|
||||||
|
/**
|
||||||
|
* @return The actual file in the underlying, i.e. decorated, CloudContentRepository
|
||||||
|
*/
|
||||||
|
val cloudFile: CloudFile
|
||||||
|
) : CloudFile, CryptoNode {
|
||||||
|
|
||||||
|
override val cloud: Cloud?
|
||||||
|
get() = parent.cloud
|
||||||
|
|
||||||
|
override val size: Long
|
||||||
|
get() = target.length.toLong()
|
||||||
|
|
||||||
|
override val modified: Date?
|
||||||
|
get() = cloudFile.modified
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other == null || javaClass != other.javaClass) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return if (other === this) {
|
||||||
|
true
|
||||||
|
} else internalEquals(other as CryptoSymlink)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun internalEquals(obj: CryptoSymlink): Boolean {
|
||||||
|
return path == obj.path
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return path.hashCode()
|
||||||
|
}
|
||||||
|
}
|
@ -1,144 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import org.cryptomator.cryptolib.api.Cryptor;
|
|
||||||
import org.cryptomator.domain.Vault;
|
|
||||||
import org.cryptomator.domain.exception.MissingCryptorException;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
import org.cryptomator.util.Supplier;
|
|
||||||
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentMap;
|
|
||||||
|
|
||||||
public abstract class Cryptors {
|
|
||||||
|
|
||||||
Cryptors() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract boolean isEmpty();
|
|
||||||
|
|
||||||
public abstract int size();
|
|
||||||
|
|
||||||
public abstract Supplier<Cryptor> get(Vault vault);
|
|
||||||
|
|
||||||
public abstract Optional<Cryptor> remove(Vault vault);
|
|
||||||
|
|
||||||
public abstract boolean putIfAbsent(Vault vault, Cryptor cryptor);
|
|
||||||
|
|
||||||
public static class Delegating extends Cryptors {
|
|
||||||
|
|
||||||
private final Cryptors.Default fallback = new Cryptors.Default();
|
|
||||||
|
|
||||||
private volatile Cryptors.Default delegate;
|
|
||||||
|
|
||||||
public synchronized void setDelegate(Cryptors.Default delegate) {
|
|
||||||
delegate.putAll(fallback.cryptors);
|
|
||||||
this.delegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void removeDelegate() {
|
|
||||||
fallback.putAll(delegate.cryptors);
|
|
||||||
this.delegate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized boolean isEmpty() {
|
|
||||||
return delegate().isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized int size() {
|
|
||||||
return delegate().size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized Supplier<Cryptor> get(Vault vault) {
|
|
||||||
return delegate().get(vault);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized Optional<Cryptor> remove(Vault vault) {
|
|
||||||
return delegate().remove(vault);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized boolean putIfAbsent(Vault vault, Cryptor cryptor) {
|
|
||||||
return delegate().putIfAbsent(vault, cryptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized Cryptors delegate() {
|
|
||||||
if (delegate == null) {
|
|
||||||
return fallback;
|
|
||||||
} else {
|
|
||||||
return delegate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Default extends Cryptors {
|
|
||||||
|
|
||||||
private final ConcurrentMap<Vault, Cryptor> cryptors = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
private Runnable onChangeListener = () -> {
|
|
||||||
};
|
|
||||||
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return cryptors.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int size() {
|
|
||||||
return cryptors.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Supplier<Cryptor> get(final Vault vault) {
|
|
||||||
return () -> {
|
|
||||||
Cryptor cryptor = cryptors.get(vault);
|
|
||||||
if (cryptor == null) {
|
|
||||||
throw new MissingCryptorException();
|
|
||||||
} else {
|
|
||||||
return cryptor;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Cryptor> remove(Vault vault) {
|
|
||||||
Optional<Cryptor> result = Optional.ofNullable(cryptors.remove(vault));
|
|
||||||
if (result.isPresent()) {
|
|
||||||
onChangeListener.run();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean putIfAbsent(Vault vault, Cryptor cryptor) {
|
|
||||||
if (cryptors.putIfAbsent(vault, cryptor) == null) {
|
|
||||||
onChangeListener.run();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnChangeListener(Runnable onChangeListener) {
|
|
||||||
this.onChangeListener = onChangeListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putAll(Map<Vault, Cryptor> cryptors) {
|
|
||||||
this.cryptors.putAll(cryptors);
|
|
||||||
onChangeListener.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void destroyAll() {
|
|
||||||
while (!isEmpty()) {
|
|
||||||
Iterator<Cryptor> cryptorIterator = cryptors.values().iterator();
|
|
||||||
while (cryptorIterator.hasNext()) {
|
|
||||||
cryptorIterator.next().destroy();
|
|
||||||
cryptorIterator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onChangeListener.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
131
data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt
Normal file
131
data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import com.google.common.base.Optional
|
||||||
|
import org.cryptomator.cryptolib.api.Cryptor
|
||||||
|
import org.cryptomator.domain.Vault
|
||||||
|
import org.cryptomator.domain.exception.MissingCryptorException
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ConcurrentMap
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
|
abstract class Cryptors internal constructor() {
|
||||||
|
|
||||||
|
abstract fun isEmpty(): Boolean
|
||||||
|
|
||||||
|
abstract fun size(): Int
|
||||||
|
|
||||||
|
abstract operator fun get(vault: Vault): Supplier<Cryptor>
|
||||||
|
|
||||||
|
abstract fun remove(vault: Vault): Optional<Cryptor>
|
||||||
|
|
||||||
|
abstract fun putIfAbsent(vault: Vault, cryptor: Cryptor): Boolean
|
||||||
|
|
||||||
|
class Delegating : Cryptors() {
|
||||||
|
|
||||||
|
private val fallback = Default()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var delegate: Default? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun setDelegate(delegate: Default) {
|
||||||
|
delegate.putAll(fallback.cryptors)
|
||||||
|
this.delegate = delegate
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun removeDelegate() {
|
||||||
|
delegate?.let {
|
||||||
|
fallback.putAll(it.cryptors)
|
||||||
|
}.also { delegate = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun isEmpty(): Boolean {
|
||||||
|
return delegate().isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun size(): Int {
|
||||||
|
return delegate().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun get(vault: Vault): Supplier<Cryptor> {
|
||||||
|
return delegate()[vault]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun remove(vault: Vault): Optional<Cryptor> {
|
||||||
|
return delegate().remove(vault)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun putIfAbsent(vault: Vault, cryptor: Cryptor): Boolean {
|
||||||
|
return delegate().putIfAbsent(vault, cryptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun delegate(): Cryptors {
|
||||||
|
return delegate ?: fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Default : Cryptors() {
|
||||||
|
|
||||||
|
val cryptors: ConcurrentMap<Vault, Cryptor> = ConcurrentHashMap()
|
||||||
|
|
||||||
|
private var onChangeListener = Runnable {}
|
||||||
|
|
||||||
|
override fun isEmpty(): Boolean {
|
||||||
|
return cryptors.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun size(): Int {
|
||||||
|
return cryptors.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(vault: Vault): Supplier<Cryptor> {
|
||||||
|
return Supplier {
|
||||||
|
cryptors[vault] ?: throw MissingCryptorException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(vault: Vault): Optional<Cryptor> {
|
||||||
|
val result = Optional.fromNullable(cryptors.remove(vault))
|
||||||
|
if (result.isPresent) {
|
||||||
|
onChangeListener.run()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putIfAbsent(vault: Vault, cryptor: Cryptor): Boolean {
|
||||||
|
return if (cryptors.putIfAbsent(vault, cryptor) == null) {
|
||||||
|
onChangeListener.run()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnChangeListener(onChangeListener: Runnable) {
|
||||||
|
this.onChangeListener = onChangeListener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAll(cryptors: Map<Vault, Cryptor>) {
|
||||||
|
this.cryptors.putAll(cryptors)
|
||||||
|
onChangeListener.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroyAll() {
|
||||||
|
while (!isEmpty()) {
|
||||||
|
val cryptorIterator = cryptors.values.iterator()
|
||||||
|
while (cryptorIterator.hasNext()) {
|
||||||
|
cryptorIterator.next().destroy()
|
||||||
|
cryptorIterator.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onChangeListener.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,38 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
|
|
||||||
interface DirIdCache {
|
|
||||||
|
|
||||||
DirIdInfo get(CryptoFolder folder);
|
|
||||||
|
|
||||||
DirIdInfo put(CryptoFolder folder, DirIdInfo dirIdInfo);
|
|
||||||
|
|
||||||
void evict(CryptoFolder folder);
|
|
||||||
|
|
||||||
void evictSubFoldersOf(CryptoFolder cryptoFolder);
|
|
||||||
|
|
||||||
class DirIdInfo {
|
|
||||||
|
|
||||||
private final String id;
|
|
||||||
private final CloudFolder cloudFolder;
|
|
||||||
|
|
||||||
DirIdInfo(String id, CloudFolder cloudFolder) {
|
|
||||||
this.id = id;
|
|
||||||
this.cloudFolder = cloudFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CloudFolder getCloudFolder() {
|
|
||||||
return cloudFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
DirIdInfo withCloudFolder(CloudFolder cloudFolder) {
|
|
||||||
return new DirIdInfo(id, cloudFolder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,21 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
|
||||||
|
interface DirIdCache {
|
||||||
|
|
||||||
|
operator fun get(folder: CryptoFolder): DirIdInfo?
|
||||||
|
|
||||||
|
fun put(folder: CryptoFolder, dirIdInfo: DirIdInfo): DirIdInfo
|
||||||
|
|
||||||
|
fun evict(folder: CryptoFolder)
|
||||||
|
|
||||||
|
fun evictSubFoldersOf(cryptoFolder: CryptoFolder)
|
||||||
|
|
||||||
|
class DirIdInfo internal constructor(val id: String, val cloudFolder: CloudFolder) {
|
||||||
|
|
||||||
|
fun withCloudFolder(cloudFolder: CloudFolder): DirIdInfo {
|
||||||
|
return DirIdInfo(id, cloudFolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,80 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import android.util.LruCache;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
class DirIdCacheFormat7 implements DirIdCache {
|
|
||||||
|
|
||||||
private static final int MAX_SIZE = 1024;
|
|
||||||
|
|
||||||
private final LruCache<DirIdCacheKey, DirIdInfo> cache = new LruCache<>(MAX_SIZE);
|
|
||||||
|
|
||||||
DirIdCacheFormat7() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DirIdInfo get(CryptoFolder folder) {
|
|
||||||
return cache.get(DirIdCacheKey.toKey(folder));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DirIdInfo put(CryptoFolder folder, DirIdInfo dirIdInfo) {
|
|
||||||
DirIdCacheKey key = DirIdCacheKey.toKey(folder);
|
|
||||||
cache.put(key, dirIdInfo);
|
|
||||||
return dirIdInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void evict(CryptoFolder folder) {
|
|
||||||
DirIdCacheKey key = DirIdCacheKey.toKey(folder);
|
|
||||||
cache.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void evictSubFoldersOf(CryptoFolder folder) {
|
|
||||||
Map<DirIdCacheKey, DirIdInfo> cacheSnapshot = cache.snapshot();
|
|
||||||
for (Map.Entry<DirIdCacheKey, DirIdInfo> cacheEntry : cacheSnapshot.entrySet()) {
|
|
||||||
if (cacheEntry.getKey().path.startsWith(folder.getPath() + "/")) {
|
|
||||||
cache.remove(cacheEntry.getKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DirIdCacheKey {
|
|
||||||
|
|
||||||
private final String path;
|
|
||||||
|
|
||||||
private DirIdCacheKey(String path) {
|
|
||||||
this.path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
static DirIdCacheKey toKey(CryptoFolder folder) {
|
|
||||||
return new DirIdCacheKey(folder.getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (obj == this) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (obj == null || getClass() != obj.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return internalEquals((DirIdCacheKey) obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean internalEquals(DirIdCacheKey o) {
|
|
||||||
return (path == null ? o.path == null : path.equals(o.path));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
final int prime = 31;
|
|
||||||
int hash = 1940604225;
|
|
||||||
hash = hash * prime + (path == null ? 0 : path.hashCode());
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,74 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import android.util.LruCache
|
||||||
|
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo
|
||||||
|
|
||||||
|
internal class DirIdCacheFormat7 : DirIdCache {
|
||||||
|
|
||||||
|
private val cache = LruCache<DirIdCacheKey, DirIdInfo>(MAX_SIZE)
|
||||||
|
|
||||||
|
override fun get(folder: CryptoFolder): DirIdInfo? {
|
||||||
|
return cache[DirIdCacheKey.toKey(folder)]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun put(folder: CryptoFolder, dirIdInfo: DirIdInfo): DirIdInfo {
|
||||||
|
val key = DirIdCacheKey.toKey(folder)
|
||||||
|
cache.put(key, dirIdInfo)
|
||||||
|
return dirIdInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun evict(folder: CryptoFolder) {
|
||||||
|
val key = DirIdCacheKey.toKey(folder)
|
||||||
|
cache.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun evictSubFoldersOf(cryptoFolder: CryptoFolder) {
|
||||||
|
val cacheSnapshot = cache.snapshot()
|
||||||
|
cacheSnapshot.forEach { (key) ->
|
||||||
|
if (key.path?.startsWith(cryptoFolder.path + "/") == true) {
|
||||||
|
cache.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DirIdCacheKey private constructor(path: String) {
|
||||||
|
|
||||||
|
val path: String?
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other === this) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return if (other == null || javaClass != other.javaClass) {
|
||||||
|
false
|
||||||
|
} else internalEquals(other as DirIdCacheKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun internalEquals(o: DirIdCacheKey): Boolean {
|
||||||
|
return if (path == null) o.path == null else path == o.path
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
val prime = 31
|
||||||
|
var hash = 1940604225
|
||||||
|
hash = hash * prime + (path?.hashCode() ?: 0)
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun toKey(folder: CryptoFolder): DirIdCacheKey {
|
||||||
|
return DirIdCacheKey(folder.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.path = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val MAX_SIZE = 1024
|
||||||
|
}
|
||||||
|
}
|
@ -1,88 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import android.util.LruCache;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
class DirIdCacheFormatPre7 implements DirIdCache {
|
|
||||||
|
|
||||||
private static final int MAX_SIZE = 1024;
|
|
||||||
|
|
||||||
private final LruCache<DirIdCacheKey, DirIdInfo> cache = new LruCache<>(MAX_SIZE);
|
|
||||||
|
|
||||||
DirIdCacheFormatPre7() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public DirIdInfo get(CryptoFolder folder) {
|
|
||||||
return cache.get(DirIdCacheKey.toKey(folder));
|
|
||||||
}
|
|
||||||
|
|
||||||
public DirIdInfo put(CryptoFolder folder, DirIdInfo dirIdInfo) {
|
|
||||||
DirIdCacheKey key = DirIdCacheKey.toKey(folder);
|
|
||||||
cache.put(key, dirIdInfo);
|
|
||||||
cache.remove(key.withoutModified());
|
|
||||||
return dirIdInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void evict(CryptoFolder folder) {
|
|
||||||
DirIdCacheKey key = DirIdCacheKey.toKey(folder);
|
|
||||||
cache.remove(key);
|
|
||||||
cache.remove(key.withoutModified());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void evictSubFoldersOf(CryptoFolder cryptoFolder) {
|
|
||||||
// no implementation needed
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DirIdCacheKey {
|
|
||||||
|
|
||||||
private final String path;
|
|
||||||
private final Date modified;
|
|
||||||
|
|
||||||
private DirIdCacheKey(CloudFile dirFile) {
|
|
||||||
this.path = dirFile == null ? null : dirFile.getPath();
|
|
||||||
this.modified = dirFile == null ? null : dirFile.getModified().orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DirIdCacheKey(String path) {
|
|
||||||
this.path = path;
|
|
||||||
this.modified = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static DirIdCacheKey toKey(CryptoFolder folder) {
|
|
||||||
return new DirIdCacheKey(folder.getDirFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
DirIdCacheKey withoutModified() {
|
|
||||||
return new DirIdCacheKey(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (obj == this) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (obj == null || getClass() != obj.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return internalEquals((DirIdCacheKey) obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean internalEquals(DirIdCacheKey o) {
|
|
||||||
return (path == null ? o.path == null : path.equals(o.path)) //
|
|
||||||
&& (modified == null ? o.modified == null : modified.equals(o.modified));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
final int prime = 31;
|
|
||||||
int hash = 1940604225;
|
|
||||||
hash = hash * prime + (path == null ? 0 : path.hashCode());
|
|
||||||
hash = hash * prime + (modified == null ? 0 : modified.hashCode());
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,86 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import android.util.LruCache
|
||||||
|
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
internal class DirIdCacheFormatPre7 : DirIdCache {
|
||||||
|
|
||||||
|
private val cache = LruCache<DirIdCacheKey, DirIdInfo>(MAX_SIZE)
|
||||||
|
|
||||||
|
override fun get(folder: CryptoFolder): DirIdInfo? {
|
||||||
|
return cache[DirIdCacheKey.toKey(folder)]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun put(folder: CryptoFolder, dirIdInfo: DirIdInfo): DirIdInfo {
|
||||||
|
val key = DirIdCacheKey.toKey(folder)
|
||||||
|
cache.put(key, dirIdInfo)
|
||||||
|
cache.remove(key.withoutModified())
|
||||||
|
return dirIdInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun evict(folder: CryptoFolder) {
|
||||||
|
val key = DirIdCacheKey.toKey(folder)
|
||||||
|
cache.remove(key)
|
||||||
|
cache.remove(key.withoutModified())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun evictSubFoldersOf(cryptoFolder: CryptoFolder) {
|
||||||
|
// no implementation needed
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DirIdCacheKey {
|
||||||
|
|
||||||
|
private val path: String?
|
||||||
|
private val modified: Date?
|
||||||
|
|
||||||
|
private constructor(dirFile: CloudFile?) {
|
||||||
|
path = dirFile?.path
|
||||||
|
modified = dirFile?.modified
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(path: String?) {
|
||||||
|
this.path = path
|
||||||
|
modified = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withoutModified(): DirIdCacheKey {
|
||||||
|
return DirIdCacheKey(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other === this) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return if (other == null || javaClass != other.javaClass) {
|
||||||
|
false
|
||||||
|
} else internalEquals(other as DirIdCacheKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun internalEquals(o: DirIdCacheKey): Boolean {
|
||||||
|
return ((if (path == null) o.path == null else path == o.path) //
|
||||||
|
&& if (modified == null) o.modified == null else modified == o.modified)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
val prime = 31
|
||||||
|
var hash = 1940604225
|
||||||
|
hash = hash * prime + (path?.hashCode() ?: 0)
|
||||||
|
hash = hash * prime + (modified?.hashCode() ?: 0)
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun toKey(folder: CryptoFolder): DirIdCacheKey {
|
||||||
|
return DirIdCacheKey(folder.dirFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val MAX_SIZE = 1024
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,293 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import com.google.common.base.Optional
|
||||||
|
import org.cryptomator.cryptolib.api.Cryptor
|
||||||
|
import org.cryptomator.cryptolib.api.CryptorProvider
|
||||||
|
import org.cryptomator.cryptolib.api.InvalidPassphraseException
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey
|
||||||
|
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException
|
||||||
|
import org.cryptomator.cryptolib.common.MasterkeyFileAccess
|
||||||
|
import org.cryptomator.data.cloud.crypto.VaultConfig.Companion.createVaultConfig
|
||||||
|
import org.cryptomator.data.cloud.crypto.VaultConfig.Companion.verify
|
||||||
|
import org.cryptomator.data.cloud.crypto.VaultConfig.VaultConfigBuilder
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||||
|
import org.cryptomator.domain.Vault
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
import org.cryptomator.domain.exception.CancellationException
|
||||||
|
import org.cryptomator.domain.exception.FatalBackendException
|
||||||
|
import org.cryptomator.domain.exception.vaultconfig.UnsupportedMasterkeyLocationException
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from
|
||||||
|
import org.cryptomator.domain.usecases.cloud.Flag
|
||||||
|
import org.cryptomator.domain.usecases.vault.UnlockToken
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.URI
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.text.Normalizer
|
||||||
|
|
||||||
|
class MasterkeyCryptoCloudProvider(
|
||||||
|
private val cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>, //
|
||||||
|
private val cryptoCloudContentRepositoryFactory: CryptoCloudContentRepositoryFactory, //
|
||||||
|
private val secureRandom: SecureRandom
|
||||||
|
) : CryptoCloudProvider {
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun create(location: CloudFolder, password: CharSequence) {
|
||||||
|
// Just for testing (id in VaultConfig is auto generated which makes sense while creating a vault but not for testing)
|
||||||
|
create(location, password, createVaultConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visible for testing
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun create(location: CloudFolder, password: CharSequence?, vaultConfigBuilder: VaultConfigBuilder) {
|
||||||
|
// 1. write masterkey:
|
||||||
|
val masterkey = Masterkey.generate(secureRandom)
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream().use { data ->
|
||||||
|
MasterkeyFileAccess(CryptoConstants.PEPPER, secureRandom).persist(masterkey, data, password, CryptoConstants.DEFAULT_MASTERKEY_FILE_VERSION)
|
||||||
|
cloudContentRepository.write(legacyMasterkeyFile(location), from(data.toByteArray()), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, data.size().toLong())
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException("Failed to write masterkey", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. initialize vault:
|
||||||
|
val vaultConfig = vaultConfigBuilder //
|
||||||
|
.vaultFormat(CryptoConstants.MAX_VAULT_VERSION) //
|
||||||
|
.cipherCombo(CryptoConstants.DEFAULT_CIPHER_COMBO) //
|
||||||
|
.keyId(URI.create(String.format("%s:%s", CryptoConstants.MASTERKEY_SCHEME, CryptoConstants.MASTERKEY_FILE_NAME))) //
|
||||||
|
.shorteningThreshold(CryptoConstants.DEFAULT_MAX_FILE_NAME) //
|
||||||
|
.build()
|
||||||
|
val encodedVaultConfig = vaultConfig.toToken(masterkey.encoded).toByteArray(StandardCharsets.UTF_8)
|
||||||
|
val vaultFile = cloudContentRepository.file(location, CryptoConstants.VAULT_FILE_NAME)
|
||||||
|
cloudContentRepository.write(vaultFile, from(encodedVaultConfig), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, encodedVaultConfig.size.toLong())
|
||||||
|
|
||||||
|
// 3. create root folder:
|
||||||
|
createRootFolder(location, cryptorFor(masterkey, vaultConfig.cipherCombo))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun createRootFolder(location: CloudFolder, cryptor: Cryptor) {
|
||||||
|
var dFolder = cloudContentRepository.folder(location, CryptoConstants.DATA_DIR_NAME)
|
||||||
|
dFolder = cloudContentRepository.create(dFolder)
|
||||||
|
val rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(CryptoConstants.ROOT_DIR_ID)
|
||||||
|
var lvl1Folder = cloudContentRepository.folder(dFolder, rootDirHash.substring(0, 2))
|
||||||
|
lvl1Folder = cloudContentRepository.create(lvl1Folder)
|
||||||
|
val lvl2Folder = cloudContentRepository.folder(lvl1Folder, rootDirHash.substring(2))
|
||||||
|
cloudContentRepository.create(lvl2Folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun unlock(vault: Vault, unverifiedVaultConfig: Optional<UnverifiedVaultConfig>, password: CharSequence, cancelledFlag: Flag): Vault {
|
||||||
|
return unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun unlock(token: UnlockToken, unverifiedVaultConfig: Optional<UnverifiedVaultConfig>, password: CharSequence, cancelledFlag: Flag): Vault {
|
||||||
|
val impl = token as UnlockTokenImpl
|
||||||
|
return try {
|
||||||
|
val masterkey = impl.getKeyFile(password)
|
||||||
|
val vaultFormat: Int
|
||||||
|
val shorteningThreshold: Int
|
||||||
|
val cryptor: Cryptor
|
||||||
|
if (unverifiedVaultConfig.isPresent) {
|
||||||
|
val vaultConfig = verify(masterkey.encoded, unverifiedVaultConfig.get())
|
||||||
|
vaultFormat = vaultConfig.vaultFormat
|
||||||
|
assertVaultVersionIsSupported(vaultConfig.vaultFormat)
|
||||||
|
shorteningThreshold = vaultConfig.shorteningThreshold
|
||||||
|
cryptor = cryptorFor(masterkey, vaultConfig.cipherCombo)
|
||||||
|
} else {
|
||||||
|
vaultFormat = MasterkeyFileAccess.readAllegedVaultVersion(impl.keyFileData)
|
||||||
|
assertLegacyVaultVersionIsSupported(vaultFormat)
|
||||||
|
shorteningThreshold = if (vaultFormat > 6) CryptoConstants.DEFAULT_MAX_FILE_NAME else CryptoImplVaultFormatPre7.SHORTENING_THRESHOLD
|
||||||
|
cryptor = cryptorFor(masterkey, CryptorProvider.Scheme.SIV_CTRMAC)
|
||||||
|
}
|
||||||
|
if (cancelledFlag.get()) {
|
||||||
|
throw CancellationException()
|
||||||
|
}
|
||||||
|
val vault = Vault.aCopyOf(token.vault) //
|
||||||
|
.withUnlocked(true) //
|
||||||
|
.withFormat(vaultFormat) //
|
||||||
|
.withShorteningThreshold(shorteningThreshold) //
|
||||||
|
.build()
|
||||||
|
cryptoCloudContentRepositoryFactory.registerCryptor(vault, cryptor)
|
||||||
|
vault
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun createUnlockToken(vault: Vault, unverifiedVaultConfig: Optional<UnverifiedVaultConfig>): UnlockTokenImpl {
|
||||||
|
val vaultLocation = vaultLocation(vault)
|
||||||
|
return if (unverifiedVaultConfig.isPresent) {
|
||||||
|
createUnlockToken(vault, masterkeyFile(vaultLocation, unverifiedVaultConfig.get()))
|
||||||
|
} else {
|
||||||
|
createUnlockToken(vault, legacyMasterkeyFile(vaultLocation))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun masterkeyFile(vaultLocation: CloudFolder, unverifiedVaultConfig: UnverifiedVaultConfig): CloudFile {
|
||||||
|
val path = unverifiedVaultConfig.keyId.schemeSpecificPart
|
||||||
|
if (path != CryptoConstants.MASTERKEY_FILE_NAME) {
|
||||||
|
throw UnsupportedMasterkeyLocationException(unverifiedVaultConfig)
|
||||||
|
}
|
||||||
|
return cloudContentRepository.file(vaultLocation, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun legacyMasterkeyFile(location: CloudFolder): CloudFile {
|
||||||
|
return cloudContentRepository.file(location, CryptoConstants.MASTERKEY_FILE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun createUnlockToken(vault: Vault, location: CloudFile): UnlockTokenImpl {
|
||||||
|
val keyFileData = readKeyFileData(location)
|
||||||
|
return UnlockTokenImpl(vault, keyFileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun readKeyFileData(masterkeyFile: CloudFile): ByteArray {
|
||||||
|
val data = ByteArrayOutputStream()
|
||||||
|
cloudContentRepository.read(masterkeyFile, null, data, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
|
||||||
|
return data.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visible for testing
|
||||||
|
fun cryptorFor(keyFile: Masterkey?, vaultCipherCombo: CryptorProvider.Scheme): Cryptor {
|
||||||
|
return CryptorProvider.forScheme(vaultCipherCombo).provide(keyFile, secureRandom)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun isVaultPasswordValid(vault: Vault, unverifiedVaultConfig: Optional<UnverifiedVaultConfig>, password: CharSequence): Boolean {
|
||||||
|
return try {
|
||||||
|
// create a cryptor, which checks the password, then destroy it immediately
|
||||||
|
val unlockToken = createUnlockToken(vault, unverifiedVaultConfig)
|
||||||
|
val masterkey = unlockToken.getKeyFile(password)
|
||||||
|
val vaultCipherCombo = if (unverifiedVaultConfig.isPresent) {
|
||||||
|
val vaultConfig = verify(masterkey.encoded, unverifiedVaultConfig.get())
|
||||||
|
assertVaultVersionIsSupported(vaultConfig.vaultFormat)
|
||||||
|
vaultConfig.cipherCombo
|
||||||
|
} else {
|
||||||
|
val vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(unlockToken.keyFileData)
|
||||||
|
assertLegacyVaultVersionIsSupported(vaultVersion)
|
||||||
|
CryptorProvider.Scheme.SIV_CTRMAC
|
||||||
|
}
|
||||||
|
cryptorFor(masterkey, vaultCipherCombo).destroy()
|
||||||
|
true
|
||||||
|
} catch (e: InvalidPassphraseException) {
|
||||||
|
false
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun lock(vault: Vault) {
|
||||||
|
cryptoCloudContentRepositoryFactory.deregisterCryptor(vault)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertVaultVersionIsSupported(version: Int) {
|
||||||
|
if (version < CryptoConstants.MIN_VAULT_VERSION) {
|
||||||
|
throw UnsupportedVaultFormatException(version, CryptoConstants.MIN_VAULT_VERSION)
|
||||||
|
} else if (version > CryptoConstants.MAX_VAULT_VERSION) {
|
||||||
|
throw UnsupportedVaultFormatException(version, CryptoConstants.MAX_VAULT_VERSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertLegacyVaultVersionIsSupported(version: Int) {
|
||||||
|
if (version < CryptoConstants.MIN_VAULT_VERSION) {
|
||||||
|
throw UnsupportedVaultFormatException(version, CryptoConstants.MIN_VAULT_VERSION)
|
||||||
|
} else if (version > CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG) {
|
||||||
|
throw UnsupportedVaultFormatException(version, CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun changePassword(vault: Vault, unverifiedVaultConfig: Optional<UnverifiedVaultConfig>, oldPassword: String, newPassword: String) {
|
||||||
|
val vaultLocation = vaultLocation(vault)
|
||||||
|
val masterkeyFile = if (unverifiedVaultConfig.isPresent) {
|
||||||
|
masterkeyFile(vaultLocation, unverifiedVaultConfig.get())
|
||||||
|
} else {
|
||||||
|
legacyMasterkeyFile(vaultLocation)
|
||||||
|
}
|
||||||
|
val dataOutputStream = ByteArrayOutputStream()
|
||||||
|
cloudContentRepository.read(masterkeyFile, null, dataOutputStream, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
|
||||||
|
val data = dataOutputStream.toByteArray()
|
||||||
|
val vaultVersion: Int
|
||||||
|
if (unverifiedVaultConfig.isPresent) {
|
||||||
|
vaultVersion = unverifiedVaultConfig.get().vaultFormat
|
||||||
|
assertVaultVersionIsSupported(vaultVersion)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(data)
|
||||||
|
assertLegacyVaultVersionIsSupported(vaultVersion)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException("Failed to read legacy vault version", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createBackupMasterKeyFile(data, masterkeyFile)
|
||||||
|
createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, masterkeyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun vaultLocation(vault: Vault): CloudFolder {
|
||||||
|
return cloudContentRepository.resolve(vault.cloud, vault.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun createBackupMasterKeyFile(data: ByteArray, masterkeyFile: CloudFile) {
|
||||||
|
cloudContentRepository.write(masterkeyBackupFile(masterkeyFile, data), from(data), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, true, data.size.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun masterkeyBackupFile(masterkeyFile: CloudFile, data: ByteArray): CloudFile {
|
||||||
|
val fileName = masterkeyFile.name + BackupFileIdSuffixGenerator.generate(data) + CryptoConstants.MASTERKEY_BACKUP_FILE_EXT
|
||||||
|
return cloudContentRepository.file(masterkeyFile.parent, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun createNewMasterKeyFile(data: ByteArray, vaultVersion: Int, oldPassword: String, newPassword: String, masterkeyFile: CloudFile) {
|
||||||
|
try {
|
||||||
|
val newMasterKeyFile = MasterkeyFileAccess(CryptoConstants.PEPPER, secureRandom) //
|
||||||
|
.changePassphrase(data, normalizePassword(oldPassword, vaultVersion), normalizePassword(newPassword, vaultVersion))
|
||||||
|
cloudContentRepository.write(
|
||||||
|
masterkeyFile, //
|
||||||
|
from(newMasterKeyFile), //
|
||||||
|
ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, //
|
||||||
|
true, //
|
||||||
|
newMasterKeyFile.size.toLong()
|
||||||
|
)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException("Failed to read legacy vault version", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizePassword(password: CharSequence, vaultVersion: Int): CharSequence {
|
||||||
|
return if (vaultVersion >= CryptoConstants.VERSION_WITH_NORMALIZED_PASSWORDS) {
|
||||||
|
Normalizer.normalize(password, Normalizer.Form.NFC)
|
||||||
|
} else {
|
||||||
|
password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnlockTokenImpl(private val vault: Vault, val keyFileData: ByteArray) : UnlockToken {
|
||||||
|
|
||||||
|
override fun getVault(): Vault {
|
||||||
|
return vault
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getKeyFile(password: CharSequence?): Masterkey {
|
||||||
|
return MasterkeyFileAccess(CryptoConstants.PEPPER, SecureRandom()).load(ByteArrayInputStream(keyFileData), password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
|
|
||||||
class RootCryptoFolder extends CryptoFolder {
|
|
||||||
|
|
||||||
private final CryptoCloud cloud;
|
|
||||||
|
|
||||||
public RootCryptoFolder(CryptoCloud cloud) {
|
|
||||||
super(null, "", "", null);
|
|
||||||
this.cloud = cloud;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isRoot(CryptoFolder folder) {
|
|
||||||
return folder instanceof RootCryptoFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return cloud;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CryptoFolder withCloud(Cloud cloud) {
|
|
||||||
return new RootCryptoFolder((CryptoCloud) cloud);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,18 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
|
||||||
|
class RootCryptoFolder(override val cloud: CryptoCloud) : CryptoFolder(null, "", "", null) {
|
||||||
|
|
||||||
|
override fun withCloud(cloud: Cloud?): CryptoFolder {
|
||||||
|
return RootCryptoFolder(cloud as CryptoCloud)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isRoot(folder: CryptoFolder): Boolean {
|
||||||
|
return folder is RootCryptoFolder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,153 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import org.cryptomator.cryptolib.api.CryptorProvider
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||||
|
import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException
|
||||||
|
import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException
|
||||||
|
import org.cryptomator.domain.exception.vaultconfig.VaultVersionMismatchException
|
||||||
|
import java.net.URI
|
||||||
|
import java.security.Key
|
||||||
|
import java.util.UUID
|
||||||
|
import io.jsonwebtoken.Claims
|
||||||
|
import io.jsonwebtoken.IncorrectClaimException
|
||||||
|
import io.jsonwebtoken.JwsHeader
|
||||||
|
import io.jsonwebtoken.JwtException
|
||||||
|
import io.jsonwebtoken.Jwts
|
||||||
|
import io.jsonwebtoken.MissingClaimException
|
||||||
|
import io.jsonwebtoken.SigningKeyResolverAdapter
|
||||||
|
import io.jsonwebtoken.security.Keys
|
||||||
|
import io.jsonwebtoken.security.SignatureException
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
class VaultConfig private constructor(builder: VaultConfigBuilder) {
|
||||||
|
|
||||||
|
val keyId: URI
|
||||||
|
val id: String
|
||||||
|
val vaultFormat: Int
|
||||||
|
val cipherCombo: CryptorProvider.Scheme
|
||||||
|
val shorteningThreshold: Int
|
||||||
|
|
||||||
|
fun toToken(rawKey: ByteArray): String {
|
||||||
|
return Jwts.builder()
|
||||||
|
.setHeaderParam(JSON_KEY_ID, keyId.toASCIIString()) //
|
||||||
|
.setId(id) //
|
||||||
|
.claim(JSON_KEY_VAULTFORMAT, vaultFormat) //
|
||||||
|
.claim(JSON_KEY_CIPHERCONFIG, cipherCombo.name) //
|
||||||
|
.claim(JSON_KEY_SHORTENING_THRESHOLD, shorteningThreshold) //
|
||||||
|
.signWith(Keys.hmacShaKeyFor(rawKey)) //
|
||||||
|
.compact()
|
||||||
|
}
|
||||||
|
|
||||||
|
class VaultConfigBuilder {
|
||||||
|
|
||||||
|
internal var id: String = UUID.randomUUID().toString()
|
||||||
|
internal var vaultFormat = CryptoConstants.MAX_VAULT_VERSION;
|
||||||
|
internal var cipherCombo = CryptoConstants.DEFAULT_CIPHER_COMBO
|
||||||
|
internal var shorteningThreshold = CryptoConstants.DEFAULT_MAX_FILE_NAME;
|
||||||
|
lateinit var keyId: URI
|
||||||
|
|
||||||
|
fun keyId(keyId: URI): VaultConfigBuilder {
|
||||||
|
this.keyId = keyId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cipherCombo(cipherCombo: CryptorProvider.Scheme): VaultConfigBuilder {
|
||||||
|
this.cipherCombo = cipherCombo
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shorteningThreshold(shorteningThreshold: Int): VaultConfigBuilder {
|
||||||
|
this.shorteningThreshold = shorteningThreshold
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun id(id: String): VaultConfigBuilder {
|
||||||
|
this.id = id
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun vaultFormat(vaultFormat: Int): VaultConfigBuilder {
|
||||||
|
this.vaultFormat = vaultFormat
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): VaultConfig {
|
||||||
|
return VaultConfig(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val JSON_KEY_VAULTFORMAT = "format"
|
||||||
|
private const val JSON_KEY_CIPHERCONFIG = "cipherCombo"
|
||||||
|
private const val JSON_KEY_SHORTENING_THRESHOLD = "shorteningThreshold"
|
||||||
|
private const val JSON_KEY_ID = "kid"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(VaultConfigLoadException::class)
|
||||||
|
fun decode(token: String): UnverifiedVaultConfig {
|
||||||
|
val unverifiedSigningKeyResolver = UnverifiedSigningKeyResolver()
|
||||||
|
|
||||||
|
// At this point we can't verify the signature because we don't have the masterkey yet.
|
||||||
|
try {
|
||||||
|
Jwts.parserBuilder().setSigningKeyResolver(unverifiedSigningKeyResolver).build().parse(token)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
return UnverifiedVaultConfig(token, unverifiedSigningKeyResolver.keyId, unverifiedSigningKeyResolver.vaultFormat)
|
||||||
|
}
|
||||||
|
throw VaultConfigLoadException("Failed to load vaultconfig")
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(VaultKeyInvalidException::class, VaultVersionMismatchException::class, VaultConfigLoadException::class)
|
||||||
|
fun verify(rawKey: ByteArray, unverifiedVaultConfig: UnverifiedVaultConfig): VaultConfig {
|
||||||
|
return try {
|
||||||
|
val parser = Jwts //
|
||||||
|
.parserBuilder() //
|
||||||
|
.setSigningKey(rawKey) //
|
||||||
|
.require(JSON_KEY_VAULTFORMAT, unverifiedVaultConfig.vaultFormat) //
|
||||||
|
.build() //
|
||||||
|
.parseClaimsJws(unverifiedVaultConfig.jwt)
|
||||||
|
|
||||||
|
val vaultConfigBuilder = createVaultConfig() //
|
||||||
|
.keyId(unverifiedVaultConfig.keyId)
|
||||||
|
.id(parser.header[JSON_KEY_ID] as String) //
|
||||||
|
.cipherCombo(CryptorProvider.Scheme.valueOf(parser.body.get(JSON_KEY_CIPHERCONFIG, String::class.java))) //
|
||||||
|
.vaultFormat(unverifiedVaultConfig.vaultFormat) //
|
||||||
|
.shorteningThreshold(parser.body[JSON_KEY_SHORTENING_THRESHOLD] as Int)
|
||||||
|
|
||||||
|
VaultConfig(vaultConfigBuilder)
|
||||||
|
} catch (e: JwtException) {
|
||||||
|
when (e) {
|
||||||
|
is MissingClaimException, is IncorrectClaimException -> throw VaultVersionMismatchException("Vault config not for version " + unverifiedVaultConfig.vaultFormat)
|
||||||
|
is SignatureException -> throw VaultKeyInvalidException()
|
||||||
|
else -> throw VaultConfigLoadException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun createVaultConfig(): VaultConfigBuilder {
|
||||||
|
return VaultConfigBuilder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UnverifiedSigningKeyResolver : SigningKeyResolverAdapter() {
|
||||||
|
|
||||||
|
lateinit var keyId: URI
|
||||||
|
var vaultFormat: Int by Delegates.notNull()
|
||||||
|
|
||||||
|
override fun resolveSigningKey(jwsHeader: JwsHeader<*>, claims: Claims): Key? {
|
||||||
|
keyId = URI.create(jwsHeader.keyId)
|
||||||
|
vaultFormat = claims[JSON_KEY_VAULTFORMAT] as Int
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
id = builder.id
|
||||||
|
keyId = builder.keyId
|
||||||
|
vaultFormat = builder.vaultFormat
|
||||||
|
cipherCombo = builder.cipherCombo
|
||||||
|
shorteningThreshold = builder.shorteningThreshold
|
||||||
|
}
|
||||||
|
}
|
@ -1,56 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.dropbox.core.DbxRequestConfig;
|
|
||||||
import com.dropbox.core.http.OkHttp3Requestor;
|
|
||||||
import com.dropbox.core.v2.DbxClientV2;
|
|
||||||
|
|
||||||
import org.cryptomator.data.BuildConfig;
|
|
||||||
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import okhttp3.Interceptor;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static org.cryptomator.data.util.NetworkTimeout.CONNECTION;
|
|
||||||
import static org.cryptomator.data.util.NetworkTimeout.READ;
|
|
||||||
import static org.cryptomator.data.util.NetworkTimeout.WRITE;
|
|
||||||
|
|
||||||
class DropboxClientFactory {
|
|
||||||
|
|
||||||
private DbxClientV2 sDbxClient;
|
|
||||||
|
|
||||||
private static Interceptor httpLoggingInterceptor(Context context) {
|
|
||||||
return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DbxClientV2 getClient(String accessToken, Context context) {
|
|
||||||
if (sDbxClient == null) {
|
|
||||||
sDbxClient = createDropboxClient(accessToken, context);
|
|
||||||
}
|
|
||||||
return sDbxClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DbxClientV2 createDropboxClient(String accessToken, Context context) {
|
|
||||||
String userLocale = Locale.getDefault().toString();
|
|
||||||
|
|
||||||
OkHttpClient okHttpClient = new OkHttpClient() //
|
|
||||||
.newBuilder() //
|
|
||||||
.connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) //
|
|
||||||
.readTimeout(READ.getTimeout(), READ.getUnit()) //
|
|
||||||
.writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) //
|
|
||||||
.addInterceptor(httpLoggingInterceptor(context)) //
|
|
||||||
.build();
|
|
||||||
|
|
||||||
DbxRequestConfig requestConfig = DbxRequestConfig //
|
|
||||||
.newBuilder("Cryptomator-Android/" + BuildConfig.VERSION_NAME) //
|
|
||||||
.withUserLocale(userLocale) //
|
|
||||||
.withHttpRequestor(new OkHttp3Requestor(okHttpClient)) //
|
|
||||||
.build();
|
|
||||||
|
|
||||||
return new DbxClientV2(requestConfig, accessToken);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,60 @@
|
|||||||
|
package org.cryptomator.data.cloud.dropbox
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.dropbox.core.DbxRequestConfig
|
||||||
|
import com.dropbox.core.http.OkHttp3Requestor
|
||||||
|
import com.dropbox.core.v2.DbxClientV2
|
||||||
|
import org.cryptomator.data.BuildConfig
|
||||||
|
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor
|
||||||
|
import org.cryptomator.data.util.NetworkTimeout
|
||||||
|
import org.cryptomator.util.crypto.CredentialCryptor
|
||||||
|
import java.util.Locale
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class DropboxClientFactory {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var instance: DbxClientV2? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getInstance(accessToken: String, context: Context): DbxClientV2 = instance ?: createDropboxClient(decrypt(accessToken, context), context).also { instance = it }
|
||||||
|
|
||||||
|
private fun decrypt(password: String, context: Context): String {
|
||||||
|
return CredentialCryptor.getInstance(context).decrypt(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDropboxClient(accessToken: String, context: Context): DbxClientV2 {
|
||||||
|
val userLocale = Locale.getDefault().toString()
|
||||||
|
|
||||||
|
val okHttpClient = OkHttpClient() //
|
||||||
|
.newBuilder() //
|
||||||
|
.connectTimeout(NetworkTimeout.CONNECTION.timeout, NetworkTimeout.CONNECTION.unit) //
|
||||||
|
.readTimeout(NetworkTimeout.READ.timeout, NetworkTimeout.READ.unit) //
|
||||||
|
.writeTimeout(NetworkTimeout.WRITE.timeout, NetworkTimeout.WRITE.unit) //
|
||||||
|
.addInterceptor(httpLoggingInterceptor(context)) //
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val requestConfig = DbxRequestConfig //
|
||||||
|
.newBuilder("Cryptomator-Android/" + BuildConfig.VERSION_NAME) //
|
||||||
|
.withUserLocale(userLocale) //
|
||||||
|
.withHttpRequestor(OkHttp3Requestor(okHttpClient)) //
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return DbxClientV2(requestConfig, accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun httpLoggingInterceptor(context: Context): Interceptor {
|
||||||
|
val logger = object : HttpLoggingInterceptor.Logger {
|
||||||
|
override fun log(message: String) {
|
||||||
|
Timber.tag("OkHttp").d(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return HttpLoggingInterceptor(logger, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,213 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.dropbox.core.DbxException;
|
|
||||||
import com.dropbox.core.InvalidAccessTokenException;
|
|
||||||
import com.dropbox.core.NetworkIOException;
|
|
||||||
import com.dropbox.core.v2.files.CreateFolderErrorException;
|
|
||||||
import com.dropbox.core.v2.files.DeleteErrorException;
|
|
||||||
import com.dropbox.core.v2.files.DownloadErrorException;
|
|
||||||
import com.dropbox.core.v2.files.ListFolderErrorException;
|
|
||||||
import com.dropbox.core.v2.files.RelocationErrorException;
|
|
||||||
|
|
||||||
import org.cryptomator.data.cloud.InterceptingCloudContentRepository;
|
|
||||||
import org.cryptomator.domain.DropboxCloud;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
|
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
|
||||||
import org.cryptomator.domain.exception.NetworkConnectionException;
|
|
||||||
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
|
||||||
import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
|
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DownloadState;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.cryptomator.util.ExceptionUtil.contains;
|
|
||||||
import static org.cryptomator.util.ExceptionUtil.extract;
|
|
||||||
|
|
||||||
class DropboxCloudContentRepository extends InterceptingCloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile> {
|
|
||||||
|
|
||||||
private final DropboxCloud cloud;
|
|
||||||
|
|
||||||
public DropboxCloudContentRepository(DropboxCloud cloud, Context context) {
|
|
||||||
super(new Intercepted(cloud, context));
|
|
||||||
this.cloud = cloud;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void throwWrappedIfRequired(Exception e) throws BackendException {
|
|
||||||
throwConnectionErrorIfRequired(e);
|
|
||||||
throwWrongCredentialsExceptionIfRequired(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException {
|
|
||||||
if (contains(e, NetworkIOException.class)) {
|
|
||||||
throw new NetworkConnectionException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void throwWrongCredentialsExceptionIfRequired(Exception e) {
|
|
||||||
if (contains(e, InvalidAccessTokenException.class)) {
|
|
||||||
throw new WrongCredentialsException(cloud);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Intercepted implements CloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile> {
|
|
||||||
|
|
||||||
private final DropboxImpl cloud;
|
|
||||||
|
|
||||||
public Intercepted(DropboxCloud cloud, Context context) {
|
|
||||||
this.cloud = new DropboxImpl(cloud, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFolder root(DropboxCloud cloud) {
|
|
||||||
return this.cloud.root();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFolder resolve(DropboxCloud cloud, String path) {
|
|
||||||
return this.cloud.resolve(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFile file(DropboxFolder parent, String name) {
|
|
||||||
return cloud.file(parent, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFile file(DropboxFolder parent, String name, Optional<Long> size) throws BackendException {
|
|
||||||
return cloud.file(parent, name, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFolder folder(DropboxFolder parent, String name) {
|
|
||||||
return cloud.folder(parent, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean exists(DropboxNode node) throws BackendException {
|
|
||||||
try {
|
|
||||||
return cloud.exists(node);
|
|
||||||
} catch (DbxException e) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<DropboxNode> list(DropboxFolder folder) throws BackendException {
|
|
||||||
try {
|
|
||||||
return cloud.list(folder);
|
|
||||||
} catch (DbxException e) {
|
|
||||||
if (e instanceof ListFolderErrorException) {
|
|
||||||
if (((ListFolderErrorException) e).errorValue.getPathValue().isNotFound()) {
|
|
||||||
throw new NoSuchCloudFileException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFolder create(DropboxFolder folder) throws BackendException {
|
|
||||||
try {
|
|
||||||
return cloud.create(folder);
|
|
||||||
} catch (DbxException e) {
|
|
||||||
if (e instanceof CreateFolderErrorException) {
|
|
||||||
throw new CloudNodeAlreadyExistsException(folder.getName());
|
|
||||||
}
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFolder move(DropboxFolder source, DropboxFolder target) throws BackendException {
|
|
||||||
try {
|
|
||||||
return (DropboxFolder) cloud.move(source, target);
|
|
||||||
} catch (DbxException e) {
|
|
||||||
if (e instanceof RelocationErrorException) {
|
|
||||||
if (extract(e, RelocationErrorException.class).get().errorValue.isFromLookup()) {
|
|
||||||
throw new NoSuchCloudFileException(source.getName());
|
|
||||||
}
|
|
||||||
throw new CloudNodeAlreadyExistsException(target.getName());
|
|
||||||
}
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFile move(DropboxFile source, DropboxFile target) throws BackendException {
|
|
||||||
try {
|
|
||||||
return (DropboxFile) cloud.move(source, target);
|
|
||||||
} catch (DbxException e) {
|
|
||||||
if (e instanceof RelocationErrorException) {
|
|
||||||
throw new CloudNodeAlreadyExistsException(target.getName());
|
|
||||||
}
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFile write(DropboxFile uploadFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
|
|
||||||
try {
|
|
||||||
return cloud.write(uploadFile, data, progressAware, replace, size);
|
|
||||||
} catch (IOException | DbxException e) {
|
|
||||||
if (contains(e, NoSuchCloudFileException.class)) {
|
|
||||||
throw new NoSuchCloudFileException(uploadFile.getName());
|
|
||||||
}
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void read(DropboxFile file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
|
||||||
try {
|
|
||||||
cloud.read(file, encryptedTmpFile, data, progressAware);
|
|
||||||
} catch (IOException | DbxException e) {
|
|
||||||
if (contains(e, DownloadErrorException.class)) {
|
|
||||||
if (extract(e, DownloadErrorException.class).get().errorValue.getPathValue().isNotFound()) {
|
|
||||||
throw new NoSuchCloudFileException(file.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(DropboxNode node) throws BackendException {
|
|
||||||
try {
|
|
||||||
cloud.delete(node);
|
|
||||||
} catch (DbxException e) {
|
|
||||||
if (contains(e, DeleteErrorException.class)) {
|
|
||||||
if (extract(e, DeleteErrorException.class).get().errorValue.getPathLookupValue().isNotFound()) {
|
|
||||||
throw new NoSuchCloudFileException(node.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String checkAuthenticationAndRetrieveCurrentAccount(DropboxCloud cloud) throws BackendException {
|
|
||||||
try {
|
|
||||||
return this.cloud.currentAccount();
|
|
||||||
} catch (DbxException e) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void logout(DropboxCloud cloud) throws BackendException {
|
|
||||||
// empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,210 @@
|
|||||||
|
package org.cryptomator.data.cloud.dropbox
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.dropbox.core.DbxException
|
||||||
|
import com.dropbox.core.InvalidAccessTokenException
|
||||||
|
import com.dropbox.core.NetworkIOException
|
||||||
|
import com.dropbox.core.v2.files.CreateFolderErrorException
|
||||||
|
import com.dropbox.core.v2.files.DeleteErrorException
|
||||||
|
import com.dropbox.core.v2.files.DownloadErrorException
|
||||||
|
import com.dropbox.core.v2.files.GetMetadataErrorException
|
||||||
|
import com.dropbox.core.v2.files.ListFolderErrorException
|
||||||
|
import com.dropbox.core.v2.files.RelocationErrorException
|
||||||
|
import org.cryptomator.data.cloud.InterceptingCloudContentRepository
|
||||||
|
import org.cryptomator.domain.DropboxCloud
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
|
||||||
|
import org.cryptomator.domain.exception.FatalBackendException
|
||||||
|
import org.cryptomator.domain.exception.NetworkConnectionException
|
||||||
|
import org.cryptomator.domain.exception.NoSuchCloudFileException
|
||||||
|
import org.cryptomator.domain.exception.authentication.WrongCredentialsException
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DownloadState
|
||||||
|
import org.cryptomator.domain.usecases.cloud.UploadState
|
||||||
|
import org.cryptomator.util.ExceptionUtil
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
internal class DropboxCloudContentRepository(private val cloud: DropboxCloud, context: Context) : InterceptingCloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile>(Intercepted(cloud, context)){
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun throwWrappedIfRequired(e: Exception) {
|
||||||
|
throwConnectionErrorIfRequired(e)
|
||||||
|
throwWrongCredentialsExceptionIfRequired(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(NetworkConnectionException::class)
|
||||||
|
private fun throwConnectionErrorIfRequired(e: Exception) {
|
||||||
|
if (ExceptionUtil.contains(e, NetworkIOException::class.java)) {
|
||||||
|
throw NetworkConnectionException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun throwWrongCredentialsExceptionIfRequired(e: Exception) {
|
||||||
|
if (ExceptionUtil.contains(e, InvalidAccessTokenException::class.java)) {
|
||||||
|
throw WrongCredentialsException(cloud)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Intercepted(cloud: DropboxCloud, context: Context) : CloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile> {
|
||||||
|
|
||||||
|
private val cloud: DropboxImpl = DropboxImpl(cloud, context)
|
||||||
|
|
||||||
|
override fun root(cloud: DropboxCloud): DropboxFolder {
|
||||||
|
return this.cloud.root()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(cloud: DropboxCloud, path: String): DropboxFolder {
|
||||||
|
return this.cloud.resolve(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun file(parent: DropboxFolder, name: String): DropboxFile {
|
||||||
|
return cloud.file(parent, name, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun file(parent: DropboxFolder, name: String, size: Long?): DropboxFile {
|
||||||
|
return cloud.file(parent, name, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun folder(parent: DropboxFolder, name: String): DropboxFolder {
|
||||||
|
return cloud.folder(parent, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun exists(node: DropboxNode): Boolean {
|
||||||
|
return try {
|
||||||
|
cloud.exists(node)
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun list(folder: DropboxFolder): List<DropboxNode> {
|
||||||
|
return try {
|
||||||
|
cloud.list(folder)
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
if (e is ListFolderErrorException) {
|
||||||
|
if (e.errorValue.pathValue.isNotFound) {
|
||||||
|
throw NoSuchCloudFileException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun create(folder: DropboxFolder): DropboxFolder {
|
||||||
|
return try {
|
||||||
|
cloud.create(folder)
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
if (e is CreateFolderErrorException) {
|
||||||
|
throw CloudNodeAlreadyExistsException(folder.name)
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: DropboxFolder, target: DropboxFolder): DropboxFolder {
|
||||||
|
return try {
|
||||||
|
cloud.move(source, target) as DropboxFolder
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
if (e is RelocationErrorException) {
|
||||||
|
if (ExceptionUtil.extract(e, RelocationErrorException::class.java).get().errorValue.isFromLookup) {
|
||||||
|
throw NoSuchCloudFileException(source.name)
|
||||||
|
}
|
||||||
|
throw CloudNodeAlreadyExistsException(target.name)
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: DropboxFile, target: DropboxFile): DropboxFile {
|
||||||
|
return try {
|
||||||
|
cloud.move(source, target) as DropboxFile
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
if (e is RelocationErrorException) {
|
||||||
|
throw CloudNodeAlreadyExistsException(target.name)
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun write(file: DropboxFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): DropboxFile {
|
||||||
|
return try {
|
||||||
|
cloud.write(file, data, progressAware, replace, size)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (ExceptionUtil.contains(e, NoSuchCloudFileException::class.java)) {
|
||||||
|
throw NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
if (ExceptionUtil.contains(e, NoSuchCloudFileException::class.java)) {
|
||||||
|
throw NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun read(file: DropboxFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
|
||||||
|
try {
|
||||||
|
cloud.read(file, encryptedTmpFile, data, progressAware)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
mapToNoSuchCloudFileExceptionIfMatches(e, file)?.let { throw it } ?: throw FatalBackendException(e)
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
mapToNoSuchCloudFileExceptionIfMatches(e, file)?.let { throw it } ?: throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapToNoSuchCloudFileExceptionIfMatches(e: Exception, file: DropboxFile) : NoSuchCloudFileException? {
|
||||||
|
if (ExceptionUtil.contains(e, GetMetadataErrorException::class.java)) {
|
||||||
|
if (ExceptionUtil.extract(e, GetMetadataErrorException::class.java).get().errorValue.pathValue.isNotFound) {
|
||||||
|
return NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ExceptionUtil.contains(e, DownloadErrorException::class.java)) {
|
||||||
|
if (ExceptionUtil.extract(e, DownloadErrorException::class.java).get().errorValue.pathValue.isNotFound) {
|
||||||
|
return NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun delete(node: DropboxNode) {
|
||||||
|
try {
|
||||||
|
cloud.delete(node)
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
if (ExceptionUtil.contains(e, DeleteErrorException::class.java)) {
|
||||||
|
if (ExceptionUtil.extract(e, DeleteErrorException::class.java).get().errorValue.pathLookupValue.isNotFound) {
|
||||||
|
throw NoSuchCloudFileException(node.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: DropboxCloud): String {
|
||||||
|
return try {
|
||||||
|
this.cloud.currentAccount()
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun logout(cloud: DropboxCloud) {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,7 @@ public class DropboxCloudContentRepositoryFactory implements CloudContentReposit
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
|
public CloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile> cloudContentRepositoryFor(Cloud cloud) {
|
||||||
return new DropboxCloudContentRepository((DropboxCloud) cloud, context);
|
return new DropboxCloudContentRepository((DropboxCloud) cloud, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import com.dropbox.core.v2.files.FileMetadata;
|
|
||||||
import com.dropbox.core.v2.files.FolderMetadata;
|
|
||||||
import com.dropbox.core.v2.files.Metadata;
|
|
||||||
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
class DropboxCloudNodeFactory {
|
|
||||||
|
|
||||||
public static DropboxFile from(DropboxFolder parent, FileMetadata metadata) {
|
|
||||||
return new DropboxFile(parent, metadata.getName(), metadata.getPathDisplay(), Optional.ofNullable(metadata.getSize()), Optional.ofNullable(metadata.getServerModified()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DropboxFile file(DropboxFolder parent, String name, Optional<Long> size, String path) {
|
|
||||||
return new DropboxFile(parent, name, path, size, Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DropboxFolder from(DropboxFolder parent, FolderMetadata metadata) {
|
|
||||||
return new DropboxFolder(parent, metadata.getName(), getNodePath(parent, metadata.getName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getNodePath(DropboxFolder parent, String name) {
|
|
||||||
return parent.getPath() + "/" + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DropboxFolder folder(DropboxFolder parent, String name, String path) {
|
|
||||||
return new DropboxFolder(parent, name, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DropboxNode from(DropboxFolder parent, Metadata metadata) {
|
|
||||||
if (metadata instanceof FileMetadata) {
|
|
||||||
return from(parent, (FileMetadata) metadata);
|
|
||||||
} else {
|
|
||||||
return from(parent, (FolderMetadata) metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,39 @@
|
|||||||
|
package org.cryptomator.data.cloud.dropbox
|
||||||
|
|
||||||
|
import com.dropbox.core.v2.files.FileMetadata
|
||||||
|
import com.dropbox.core.v2.files.FolderMetadata
|
||||||
|
import com.dropbox.core.v2.files.Metadata
|
||||||
|
|
||||||
|
internal object DropboxCloudNodeFactory {
|
||||||
|
|
||||||
|
fun from(parent: DropboxFolder, metadata: FileMetadata): DropboxFile {
|
||||||
|
return DropboxFile(parent, metadata.name, metadata.pathDisplay, metadata.size, metadata.serverModified)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun file(parent: DropboxFolder, name: String, size: Long?, path: String): DropboxFile {
|
||||||
|
return DropboxFile(parent, name, path, size, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun from(parent: DropboxFolder, metadata: FolderMetadata): DropboxFolder {
|
||||||
|
return DropboxFolder(parent, metadata.name, getNodePath(parent, metadata.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNodePath(parent: DropboxFolder, name: String): String {
|
||||||
|
return parent.path + "/" + name
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun folder(parent: DropboxFolder?, name: String, path: String): DropboxFolder {
|
||||||
|
return DropboxFolder(parent, name, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun from(parent: DropboxFolder, metadata: Metadata): DropboxNode {
|
||||||
|
return if (metadata is FileMetadata) {
|
||||||
|
from(parent, metadata)
|
||||||
|
} else {
|
||||||
|
from(parent, metadata as FolderMetadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,55 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
class DropboxFile implements CloudFile, DropboxNode {
|
|
||||||
|
|
||||||
private final DropboxFolder parent;
|
|
||||||
private final String name;
|
|
||||||
private final String path;
|
|
||||||
private final Optional<Long> size;
|
|
||||||
private final Optional<Date> modified;
|
|
||||||
|
|
||||||
public DropboxFile(DropboxFolder parent, String name, String path, Optional<Long> size, Optional<Date> modified) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
this.size = size;
|
|
||||||
this.modified = modified;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return parent.getCloud();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFolder getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Long> getSize() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Date> getModified() {
|
|
||||||
return modified;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,11 @@
|
|||||||
|
package org.cryptomator.data.cloud.dropbox
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
internal class DropboxFile(override val parent: DropboxFolder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, DropboxNode {
|
||||||
|
|
||||||
|
override val cloud: Cloud?
|
||||||
|
get() = parent.cloud
|
||||||
|
}
|
@ -1,42 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
|
|
||||||
class DropboxFolder implements CloudFolder, DropboxNode {
|
|
||||||
|
|
||||||
private final DropboxFolder parent;
|
|
||||||
private final String name;
|
|
||||||
private final String path;
|
|
||||||
|
|
||||||
public DropboxFolder(DropboxFolder parent, String name, String path) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return parent.getCloud();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFolder getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFolder withCloud(Cloud cloud) {
|
|
||||||
return new DropboxFolder(parent.withCloud(cloud), name, path);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,14 @@
|
|||||||
|
package org.cryptomator.data.cloud.dropbox
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
|
||||||
|
open class DropboxFolder(override val parent: DropboxFolder?, override val name: String, override val path: String) : CloudFolder, DropboxNode {
|
||||||
|
|
||||||
|
override val cloud: Cloud?
|
||||||
|
get() = parent?.cloud
|
||||||
|
|
||||||
|
override fun withCloud(cloud: Cloud?): DropboxFolder? {
|
||||||
|
return DropboxFolder(parent?.withCloud(cloud), name, path)
|
||||||
|
}
|
||||||
|
}
|
@ -1,466 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.dropbox.core.DbxException;
|
|
||||||
import com.dropbox.core.NetworkIOException;
|
|
||||||
import com.dropbox.core.RetryException;
|
|
||||||
import com.dropbox.core.v2.DbxClientV2;
|
|
||||||
import com.dropbox.core.v2.files.CommitInfo;
|
|
||||||
import com.dropbox.core.v2.files.CreateFolderResult;
|
|
||||||
import com.dropbox.core.v2.files.FileMetadata;
|
|
||||||
import com.dropbox.core.v2.files.FolderMetadata;
|
|
||||||
import com.dropbox.core.v2.files.GetMetadataErrorException;
|
|
||||||
import com.dropbox.core.v2.files.ListFolderResult;
|
|
||||||
import com.dropbox.core.v2.files.Metadata;
|
|
||||||
import com.dropbox.core.v2.files.RelocationResult;
|
|
||||||
import com.dropbox.core.v2.files.UploadSessionCursor;
|
|
||||||
import com.dropbox.core.v2.files.UploadSessionFinishErrorException;
|
|
||||||
import com.dropbox.core.v2.files.UploadSessionLookupErrorException;
|
|
||||||
import com.dropbox.core.v2.files.WriteMode;
|
|
||||||
import com.dropbox.core.v2.users.FullAccount;
|
|
||||||
import com.tomclaw.cache.DiskLruCache;
|
|
||||||
|
|
||||||
import org.cryptomator.data.util.TransferredBytesAwareInputStream;
|
|
||||||
import org.cryptomator.data.util.TransferredBytesAwareOutputStream;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
import org.cryptomator.domain.DropboxCloud;
|
|
||||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
|
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
|
||||||
import org.cryptomator.domain.exception.authentication.AuthenticationException;
|
|
||||||
import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DownloadState;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.Progress;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
import org.cryptomator.util.SharedPreferencesHandler;
|
|
||||||
import org.cryptomator.util.crypto.CredentialCryptor;
|
|
||||||
import org.cryptomator.util.file.LruFileCacheUtil;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
|
|
||||||
import static org.cryptomator.util.file.LruFileCacheUtil.Cache.DROPBOX;
|
|
||||||
import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache;
|
|
||||||
import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache;
|
|
||||||
|
|
||||||
class DropboxImpl {
|
|
||||||
|
|
||||||
private static final long CHUNKED_UPLOAD_CHUNK_SIZE = 8L << 20;
|
|
||||||
private static final int CHUNKED_UPLOAD_MAX_ATTEMPTS = 5;
|
|
||||||
|
|
||||||
private final DropboxClientFactory clientFactory = new DropboxClientFactory();
|
|
||||||
private final DropboxCloud cloud;
|
|
||||||
private final RootDropboxFolder root;
|
|
||||||
private final Context context;
|
|
||||||
private final SharedPreferencesHandler sharedPreferencesHandler;
|
|
||||||
|
|
||||||
private DiskLruCache diskLruCache;
|
|
||||||
|
|
||||||
DropboxImpl(DropboxCloud cloud, Context context) {
|
|
||||||
if (cloud.accessToken() == null) {
|
|
||||||
throw new NoAuthenticationProvidedException(cloud);
|
|
||||||
}
|
|
||||||
this.cloud = cloud;
|
|
||||||
this.root = new RootDropboxFolder(cloud);
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
sharedPreferencesHandler = new SharedPreferencesHandler(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sleepQuietly(long millis) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(millis);
|
|
||||||
} catch (InterruptedException ex) {
|
|
||||||
throw new FatalBackendException("Error uploading to Dropbox: interrupted during backoff.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private DbxClientV2 client() throws AuthenticationException {
|
|
||||||
return clientFactory.getClient(decrypt(cloud.accessToken()), context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String decrypt(String password) {
|
|
||||||
return CredentialCryptor //
|
|
||||||
.getInstance(context) //
|
|
||||||
.decrypt(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFolder root() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFolder resolve(String path) {
|
|
||||||
if (path.startsWith("/")) {
|
|
||||||
path = path.substring(1);
|
|
||||||
}
|
|
||||||
String[] names = path.split("/");
|
|
||||||
DropboxFolder folder = root;
|
|
||||||
for (String name : names) {
|
|
||||||
folder = folder(folder, name);
|
|
||||||
}
|
|
||||||
return folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFile file(CloudFolder folder, String name) {
|
|
||||||
return file(folder, name, Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFile file(CloudFolder folder, String name, Optional<Long> size) {
|
|
||||||
return DropboxCloudNodeFactory.file( //
|
|
||||||
(DropboxFolder) folder, //
|
|
||||||
name, //
|
|
||||||
size, //
|
|
||||||
folder.getPath() + '/' + name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFolder folder(CloudFolder folder, String name) {
|
|
||||||
return DropboxCloudNodeFactory.folder( //
|
|
||||||
(DropboxFolder) folder, //
|
|
||||||
name, //
|
|
||||||
folder.getPath() + '/' + name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean exists(CloudNode node) throws AuthenticationException, DbxException {
|
|
||||||
try {
|
|
||||||
Metadata metadata = client() //
|
|
||||||
.files() //
|
|
||||||
.getMetadata(node.getPath());
|
|
||||||
if (node instanceof CloudFolder) {
|
|
||||||
return metadata instanceof FolderMetadata;
|
|
||||||
} else {
|
|
||||||
return metadata instanceof FileMetadata;
|
|
||||||
}
|
|
||||||
} catch (GetMetadataErrorException e) {
|
|
||||||
if (e.errorValue.isPath()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<DropboxNode> list(CloudFolder folder) throws AuthenticationException, DbxException {
|
|
||||||
List<DropboxNode> result = new ArrayList<>();
|
|
||||||
ListFolderResult listFolderResult = null;
|
|
||||||
do {
|
|
||||||
if (listFolderResult == null) {
|
|
||||||
listFolderResult = client() //
|
|
||||||
.files() //
|
|
||||||
.listFolder(folder.getPath());
|
|
||||||
} else {
|
|
||||||
String cursor = listFolderResult.getCursor();
|
|
||||||
listFolderResult = client() //
|
|
||||||
.files() //
|
|
||||||
.listFolderContinue(cursor);
|
|
||||||
}
|
|
||||||
List<Metadata> entryMetadata = listFolderResult.getEntries();
|
|
||||||
for (Metadata metadata : entryMetadata) {
|
|
||||||
result.add(DropboxCloudNodeFactory.from( //
|
|
||||||
(DropboxFolder) folder, //
|
|
||||||
metadata));
|
|
||||||
}
|
|
||||||
} while (listFolderResult.getHasMore());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFolder create(CloudFolder folder) throws AuthenticationException, DbxException {
|
|
||||||
CreateFolderResult createFolderResult = client() //
|
|
||||||
.files() //
|
|
||||||
.createFolderV2(folder.getPath());
|
|
||||||
|
|
||||||
return DropboxCloudNodeFactory.from( //
|
|
||||||
(DropboxFolder) folder.getParent(), //
|
|
||||||
createFolderResult.getMetadata());
|
|
||||||
}
|
|
||||||
|
|
||||||
public CloudNode move(CloudNode source, CloudNode target) throws AuthenticationException, DbxException {
|
|
||||||
RelocationResult relocationResult = client() //
|
|
||||||
.files() //
|
|
||||||
.moveV2(source.getPath(), target.getPath());
|
|
||||||
|
|
||||||
return DropboxCloudNodeFactory.from( //
|
|
||||||
(DropboxFolder) target.getParent(), //
|
|
||||||
relocationResult.getMetadata());
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFile write(DropboxFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, long size) throws AuthenticationException, DbxException, IOException, CloudNodeAlreadyExistsException {
|
|
||||||
if (exists(file) && !replace) {
|
|
||||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
|
||||||
}
|
|
||||||
|
|
||||||
progressAware.onProgress(Progress.started(UploadState.upload(file)));
|
|
||||||
WriteMode writeMode = WriteMode.ADD;
|
|
||||||
if (replace) {
|
|
||||||
writeMode = WriteMode.OVERWRITE;
|
|
||||||
}
|
|
||||||
// "Upload the file with simple upload API if it is small enough, otherwise use chunked
|
|
||||||
// upload API for better performance. Arbitrarily chose 2 times our chunk size as the
|
|
||||||
// deciding factor. This should really depend on your network."
|
|
||||||
// Source: https://github.com/dropbox/dropbox-sdk-java/blob/master/examples/upload-file/src/main/java/com/dropbox/core/examples/upload_file/Main.java
|
|
||||||
if (size <= (2 * CHUNKED_UPLOAD_CHUNK_SIZE)) {
|
|
||||||
uploadFile(file, data, progressAware, writeMode, size);
|
|
||||||
} else {
|
|
||||||
chunkedUploadFile(file, data, progressAware, writeMode, size);
|
|
||||||
}
|
|
||||||
FileMetadata metadata = (FileMetadata) client() //
|
|
||||||
.files() //
|
|
||||||
.getMetadata(file.getPath());
|
|
||||||
|
|
||||||
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
|
|
||||||
|
|
||||||
return DropboxCloudNodeFactory.from( //
|
|
||||||
file.getParent(), //
|
|
||||||
metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void uploadFile(final DropboxFile file, DataSource data, final ProgressAware<UploadState> progressAware, WriteMode writeMode, final long size) //
|
|
||||||
throws AuthenticationException, DbxException, IOException {
|
|
||||||
try (TransferredBytesAwareInputStream in = new TransferredBytesAwareInputStream(data.open(context)) {
|
|
||||||
@Override
|
|
||||||
public void bytesTransferred(long transferred) {
|
|
||||||
progressAware.onProgress( //
|
|
||||||
progress(UploadState.upload(file)) //
|
|
||||||
.between(0) //
|
|
||||||
.and(size) //
|
|
||||||
.withValue(transferred));
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
client() //
|
|
||||||
.files() //
|
|
||||||
.uploadBuilder(file.getPath()) //
|
|
||||||
.withMode(writeMode) //
|
|
||||||
.uploadAndFinish(in);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void chunkedUploadFile(final DropboxFile file, DataSource data, final ProgressAware<UploadState> progressAware, WriteMode writeMode, final long size) throws AuthenticationException, DbxException, IOException {
|
|
||||||
// Assert our file is at least the chunk upload size. We make this assumption in the code
|
|
||||||
// below to simplify the logic.
|
|
||||||
if (size < CHUNKED_UPLOAD_CHUNK_SIZE) {
|
|
||||||
throw new FatalBackendException("File too small, use uploadFile() instead.");
|
|
||||||
}
|
|
||||||
|
|
||||||
long uploaded = 0L;
|
|
||||||
DbxException thrown = null;
|
|
||||||
|
|
||||||
try (InputStream stream = data.open(context)) {
|
|
||||||
|
|
||||||
// Chunked uploads have 3 phases, each of which can accept uploaded bytes:
|
|
||||||
//
|
|
||||||
// (1) Start: initiate the upload and get an upload session ID
|
|
||||||
// (2) Append: upload chunks of the file to append to our session
|
|
||||||
// (3) Finish: commit the upload and close the session
|
|
||||||
//
|
|
||||||
// We track how many bytes we uploaded to determine which phase we should be in.
|
|
||||||
String sessionId = null;
|
|
||||||
for (int i = 0; i < CHUNKED_UPLOAD_MAX_ATTEMPTS; i++) {
|
|
||||||
if (i > 0) {
|
|
||||||
Timber.v("Retrying chunked upload (" + (i + 1) + " / " + CHUNKED_UPLOAD_MAX_ATTEMPTS + " attempts)");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// if this is a retry, make sure seek to the correct offset
|
|
||||||
stream.skip(uploaded);
|
|
||||||
|
|
||||||
// (1) Start
|
|
||||||
if (sessionId == null) {
|
|
||||||
sessionId = client() //
|
|
||||||
.files() //
|
|
||||||
.uploadSessionStart() //
|
|
||||||
.uploadAndFinish(new TransferredBytesAwareInputStream(stream) {
|
|
||||||
@Override
|
|
||||||
public void bytesTransferred(long transferred) {
|
|
||||||
progressAware.onProgress( //
|
|
||||||
progress(UploadState.upload(file)) //
|
|
||||||
.between(0) //
|
|
||||||
.and(size) //
|
|
||||||
.withValue(transferred));
|
|
||||||
}
|
|
||||||
}, CHUNKED_UPLOAD_CHUNK_SIZE).getSessionId();
|
|
||||||
uploaded += CHUNKED_UPLOAD_CHUNK_SIZE;
|
|
||||||
|
|
||||||
progressAware.onProgress( //
|
|
||||||
progress(UploadState.upload(file)) //
|
|
||||||
.between(0) //
|
|
||||||
.and(size) //
|
|
||||||
.withValue(uploaded));
|
|
||||||
}
|
|
||||||
|
|
||||||
UploadSessionCursor cursor = new UploadSessionCursor(sessionId, uploaded);
|
|
||||||
|
|
||||||
// (2) Append
|
|
||||||
while ((size - uploaded) > CHUNKED_UPLOAD_CHUNK_SIZE) {
|
|
||||||
final long fullyUploaded = uploaded;
|
|
||||||
client() //
|
|
||||||
.files() //
|
|
||||||
.uploadSessionAppendV2(cursor) //
|
|
||||||
.uploadAndFinish(new TransferredBytesAwareInputStream(stream) {
|
|
||||||
@Override
|
|
||||||
public void bytesTransferred(long transferred) {
|
|
||||||
progressAware.onProgress( //
|
|
||||||
progress(UploadState.upload(file)) //
|
|
||||||
.between(0) //
|
|
||||||
.and(size) //
|
|
||||||
.withValue(fullyUploaded + transferred));
|
|
||||||
}
|
|
||||||
}, CHUNKED_UPLOAD_CHUNK_SIZE);
|
|
||||||
uploaded += CHUNKED_UPLOAD_CHUNK_SIZE;
|
|
||||||
|
|
||||||
progressAware.onProgress( //
|
|
||||||
progress(UploadState.upload(file)) //
|
|
||||||
.between(0) //
|
|
||||||
.and(size) //
|
|
||||||
.withValue(uploaded));
|
|
||||||
|
|
||||||
cursor = new UploadSessionCursor(sessionId, uploaded);
|
|
||||||
}
|
|
||||||
|
|
||||||
// (3) Finish
|
|
||||||
long remaining = size - uploaded;
|
|
||||||
CommitInfo commitInfo = CommitInfo //
|
|
||||||
.newBuilder(file.getPath()) //
|
|
||||||
.withMode(writeMode) //
|
|
||||||
.build();
|
|
||||||
|
|
||||||
client() //
|
|
||||||
.files() //
|
|
||||||
.uploadSessionFinish(cursor, commitInfo) //
|
|
||||||
.uploadAndFinish(stream, remaining);
|
|
||||||
|
|
||||||
return;
|
|
||||||
} catch (RetryException ex) {
|
|
||||||
thrown = ex;
|
|
||||||
// RetryExceptions are never automatically retried by the client for uploads. Must
|
|
||||||
// catch this exception even if DbxRequestConfig.getMaxRetries() > 0.
|
|
||||||
sleepQuietly(ex.getBackoffMillis());
|
|
||||||
} catch (NetworkIOException ex) {
|
|
||||||
thrown = ex;
|
|
||||||
// Network issue with Dropbox (maybe a timeout?), try again.
|
|
||||||
} catch (UploadSessionLookupErrorException ex) {
|
|
||||||
if (ex.errorValue.isIncorrectOffset()) {
|
|
||||||
thrown = ex;
|
|
||||||
// Server offset into the stream doesn't match our offset (uploaded). Seek to
|
|
||||||
// the expected offset according to the server and try again.
|
|
||||||
uploaded = ex. //
|
|
||||||
errorValue. //
|
|
||||||
getIncorrectOffsetValue(). //
|
|
||||||
getCorrectOffset();
|
|
||||||
} else {
|
|
||||||
throw new FatalBackendException(ex);
|
|
||||||
}
|
|
||||||
} catch (UploadSessionFinishErrorException ex) {
|
|
||||||
if (ex.errorValue.isLookupFailed() && ex.errorValue.getLookupFailedValue().isIncorrectOffset()) {
|
|
||||||
thrown = ex;
|
|
||||||
// Server offset into the stream doesn't match our offset (uploaded). Seek to
|
|
||||||
// the expected offset according to the server and try again.
|
|
||||||
uploaded = ex. //
|
|
||||||
errorValue. //
|
|
||||||
getLookupFailedValue(). //
|
|
||||||
getIncorrectOffsetValue(). //
|
|
||||||
getCorrectOffset();
|
|
||||||
} else {
|
|
||||||
throw new FatalBackendException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new FatalBackendException("Maxed out upload attempts to Dropbox.", thrown);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void read(CloudFile file, Optional<File> encryptedTmpFile, OutputStream data, final ProgressAware<DownloadState> progressAware) throws DbxException, IOException {
|
|
||||||
progressAware.onProgress(Progress.started(DownloadState.download(file)));
|
|
||||||
|
|
||||||
Optional<String> cacheKey = Optional.empty();
|
|
||||||
Optional<File> cacheFile = Optional.empty();
|
|
||||||
|
|
||||||
if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) {
|
|
||||||
final FileMetadata fileMetadata = (FileMetadata) client() //
|
|
||||||
.files() //
|
|
||||||
.getMetadata(file.getPath());
|
|
||||||
cacheKey = Optional.of(fileMetadata.getId() + fileMetadata.getRev());
|
|
||||||
java.io.File cachedFile = diskLruCache.get(cacheKey.get());
|
|
||||||
cacheFile = cachedFile != null ? Optional.of(cachedFile) : Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sharedPreferencesHandler.useLruCache() && cacheFile.isPresent()) {
|
|
||||||
try {
|
|
||||||
retrieveFromLruCache(cacheFile.get(), data);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.tag("DropboxImpl").w(e, "Error while retrieving content from Cache, get from web request");
|
|
||||||
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware);
|
|
||||||
}
|
|
||||||
|
|
||||||
progressAware.onProgress(Progress.completed(DownloadState.download(file)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeToData(final CloudFile file, //
|
|
||||||
final OutputStream data, //
|
|
||||||
final Optional<File> encryptedTmpFile, //
|
|
||||||
final Optional<String> cacheKey, //
|
|
||||||
final ProgressAware<DownloadState> progressAware) throws DbxException, IOException {
|
|
||||||
try (TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) {
|
|
||||||
@Override
|
|
||||||
public void bytesTransferred(long transferred) {
|
|
||||||
progressAware.onProgress( //
|
|
||||||
progress(DownloadState.download(file)) //
|
|
||||||
.between(0) //
|
|
||||||
.and(file.getSize().orElse(Long.MAX_VALUE)) //
|
|
||||||
.withValue(transferred));
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
client() //
|
|
||||||
.files() //
|
|
||||||
.download(file.getPath()) //
|
|
||||||
.download(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) {
|
|
||||||
try {
|
|
||||||
storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get());
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.tag("DropboxImpl").e(e, "Failed to write downloaded file in LRU cache");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean createLruCache(int cacheSize) {
|
|
||||||
if (diskLruCache == null) {
|
|
||||||
try {
|
|
||||||
diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(DROPBOX), cacheSize);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.tag("DropboxImpl").e(e, "Failed to setup LRU cache");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete(CloudNode node) throws AuthenticationException, DbxException {
|
|
||||||
client() //
|
|
||||||
.files() //
|
|
||||||
.deleteV2(node.getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String currentAccount() throws AuthenticationException, DbxException {
|
|
||||||
FullAccount currentAccount = client() //
|
|
||||||
.users() //
|
|
||||||
.getCurrentAccount();
|
|
||||||
return currentAccount.getName().getDisplayName();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,397 @@
|
|||||||
|
package org.cryptomator.data.cloud.dropbox
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.dropbox.core.DbxException
|
||||||
|
import com.dropbox.core.NetworkIOException
|
||||||
|
import com.dropbox.core.RetryException
|
||||||
|
import com.dropbox.core.v2.DbxClientV2
|
||||||
|
import com.dropbox.core.v2.files.CommitInfo
|
||||||
|
import com.dropbox.core.v2.files.FileMetadata
|
||||||
|
import com.dropbox.core.v2.files.FolderMetadata
|
||||||
|
import com.dropbox.core.v2.files.GetMetadataErrorException
|
||||||
|
import com.dropbox.core.v2.files.ListFolderResult
|
||||||
|
import com.dropbox.core.v2.files.UploadSessionCursor
|
||||||
|
import com.dropbox.core.v2.files.UploadSessionFinishErrorException
|
||||||
|
import com.dropbox.core.v2.files.UploadSessionLookupErrorException
|
||||||
|
import com.dropbox.core.v2.files.WriteMode
|
||||||
|
import com.tomclaw.cache.DiskLruCache
|
||||||
|
import org.cryptomator.data.cloud.dropbox.DropboxCloudNodeFactory.file
|
||||||
|
import org.cryptomator.data.cloud.dropbox.DropboxCloudNodeFactory.folder
|
||||||
|
import org.cryptomator.data.cloud.dropbox.DropboxCloudNodeFactory.from
|
||||||
|
import org.cryptomator.data.util.TransferredBytesAwareInputStream
|
||||||
|
import org.cryptomator.data.util.TransferredBytesAwareOutputStream
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
import org.cryptomator.domain.DropboxCloud
|
||||||
|
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
|
||||||
|
import org.cryptomator.domain.exception.FatalBackendException
|
||||||
|
import org.cryptomator.domain.exception.ParentFolderIsNullException
|
||||||
|
import org.cryptomator.domain.exception.authentication.AuthenticationException
|
||||||
|
import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DownloadState
|
||||||
|
import org.cryptomator.domain.usecases.cloud.Progress
|
||||||
|
import org.cryptomator.domain.usecases.cloud.UploadState
|
||||||
|
import org.cryptomator.util.SharedPreferencesHandler
|
||||||
|
import org.cryptomator.util.file.LruFileCacheUtil
|
||||||
|
import org.cryptomator.util.file.LruFileCacheUtil.Companion.retrieveFromLruCache
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.ArrayList
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
internal class DropboxImpl(cloud: DropboxCloud, context: Context) {
|
||||||
|
|
||||||
|
private val cloud: DropboxCloud
|
||||||
|
private val root: RootDropboxFolder
|
||||||
|
private val context: Context
|
||||||
|
private val sharedPreferencesHandler: SharedPreferencesHandler
|
||||||
|
private var diskLruCache: DiskLruCache? = null
|
||||||
|
|
||||||
|
@Throws(AuthenticationException::class)
|
||||||
|
private fun client(): DbxClientV2 {
|
||||||
|
return DropboxClientFactory.getInstance(cloud.accessToken(), context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun root(): DropboxFolder {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolve(path: String): DropboxFolder {
|
||||||
|
val names = path.removePrefix("/").split("/").toTypedArray()
|
||||||
|
var folder: DropboxFolder = root
|
||||||
|
for (name in names) {
|
||||||
|
folder = folder(folder, name)
|
||||||
|
}
|
||||||
|
return folder
|
||||||
|
}
|
||||||
|
|
||||||
|
fun file(folder: DropboxFolder, name: String, size: Long?): DropboxFile {
|
||||||
|
return file(folder, name, size, folder.path + '/' + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun folder(folder: DropboxFolder, name: String): DropboxFolder {
|
||||||
|
return folder(folder, name, folder.path + '/' + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(AuthenticationException::class, DbxException::class)
|
||||||
|
fun exists(node: CloudNode): Boolean {
|
||||||
|
return try {
|
||||||
|
val metadata = client() //
|
||||||
|
.files() //
|
||||||
|
.getMetadata(node.path)
|
||||||
|
if (node is CloudFolder) {
|
||||||
|
metadata is FolderMetadata
|
||||||
|
} else {
|
||||||
|
metadata is FileMetadata
|
||||||
|
}
|
||||||
|
} catch (e: GetMetadataErrorException) {
|
||||||
|
if (e.errorValue.isPath) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(AuthenticationException::class, DbxException::class)
|
||||||
|
fun list(folder: DropboxFolder): List<DropboxNode> {
|
||||||
|
val result: MutableList<DropboxNode> = ArrayList()
|
||||||
|
var listFolderResult: ListFolderResult? = null
|
||||||
|
do {
|
||||||
|
listFolderResult = if (listFolderResult == null) {
|
||||||
|
client().files().listFolder(folder.path)
|
||||||
|
} else {
|
||||||
|
client().files().listFolderContinue(listFolderResult.cursor)
|
||||||
|
}
|
||||||
|
listFolderResult.entries.parallelStream().forEach {
|
||||||
|
result.add(from(folder, it))
|
||||||
|
}
|
||||||
|
} while (listFolderResult?.hasMore == true)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(AuthenticationException::class, DbxException::class)
|
||||||
|
fun create(folder: DropboxFolder): DropboxFolder {
|
||||||
|
folder.parent?.let {
|
||||||
|
val createFolderResult = client().files().createFolderV2(folder.path)
|
||||||
|
return from(it, createFolderResult.metadata)
|
||||||
|
} ?: throw ParentFolderIsNullException(folder.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(AuthenticationException::class, DbxException::class)
|
||||||
|
fun move(source: DropboxNode, target: DropboxNode): DropboxNode {
|
||||||
|
target.parent?.let { targetsParent ->
|
||||||
|
val relocationResult = client().files().moveV2(source.path, target.path)
|
||||||
|
return from(targetsParent, relocationResult.metadata)
|
||||||
|
} ?: throw ParentFolderIsNullException(target.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(AuthenticationException::class, DbxException::class, IOException::class, CloudNodeAlreadyExistsException::class)
|
||||||
|
fun write(file: DropboxFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): DropboxFile {
|
||||||
|
if (!replace && exists(file)) {
|
||||||
|
throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false")
|
||||||
|
}
|
||||||
|
progressAware.onProgress(Progress.started(UploadState.upload(file)))
|
||||||
|
var writeMode = WriteMode.ADD
|
||||||
|
if (replace) {
|
||||||
|
writeMode = WriteMode.OVERWRITE
|
||||||
|
}
|
||||||
|
// "Upload the file with simple upload API if it is small enough, otherwise use chunked
|
||||||
|
// upload API for better performance. Arbitrarily chose 2 times our chunk size as the
|
||||||
|
// deciding factor. This should really depend on your network."
|
||||||
|
// Source: https://github.com/dropbox/dropbox-sdk-java/blob/master/examples/upload-file/src/main/java/com/dropbox/core/examples/upload_file/Main.java
|
||||||
|
if (size <= 2 * CHUNKED_UPLOAD_CHUNK_SIZE) {
|
||||||
|
uploadFile(file, data, progressAware, writeMode, size)
|
||||||
|
} else {
|
||||||
|
chunkedUploadFile(file, data, progressAware, writeMode, size)
|
||||||
|
}
|
||||||
|
val metadata = client().files().getMetadata(file.path)
|
||||||
|
progressAware.onProgress(Progress.completed(UploadState.upload(file)))
|
||||||
|
return from(file.parent, metadata) as DropboxFile
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(AuthenticationException::class, DbxException::class, IOException::class)
|
||||||
|
private fun uploadFile(file: DropboxFile, data: DataSource, progressAware: ProgressAware<UploadState>, writeMode: WriteMode, size: Long) {
|
||||||
|
data.open(context)?.use { inputStream ->
|
||||||
|
object : TransferredBytesAwareInputStream(inputStream) {
|
||||||
|
override fun bytesTransferred(transferred: Long) {
|
||||||
|
progressAware.onProgress( //
|
||||||
|
Progress.progress(UploadState.upload(file)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(size) //
|
||||||
|
.withValue(transferred)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.use {
|
||||||
|
client() //
|
||||||
|
.files() //
|
||||||
|
.uploadBuilder(file.path) //
|
||||||
|
.withMode(writeMode) //
|
||||||
|
.uploadAndFinish(it)
|
||||||
|
}
|
||||||
|
} ?: Timber.tag("").e("InputStream shouldn't be null")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(AuthenticationException::class, DbxException::class, IOException::class)
|
||||||
|
private fun chunkedUploadFile(file: DropboxFile, data: DataSource, progressAware: ProgressAware<UploadState>, writeMode: WriteMode, size: Long) {
|
||||||
|
// Assert our file is at least the chunk upload size. We make this assumption in the code
|
||||||
|
// below to simplify the logic.
|
||||||
|
if (size < CHUNKED_UPLOAD_CHUNK_SIZE) {
|
||||||
|
throw FatalBackendException("File too small, use uploadFile() instead.")
|
||||||
|
}
|
||||||
|
var uploaded = 0L
|
||||||
|
var thrown: DbxException? = null
|
||||||
|
data.open(context)?.use {
|
||||||
|
|
||||||
|
// Chunked uploads have 3 phases, each of which can accept uploaded bytes:
|
||||||
|
//
|
||||||
|
// (1) Start: initiate the upload and get an upload session ID
|
||||||
|
// (2) Append: upload chunks of the file to append to our session
|
||||||
|
// (3) Finish: commit the upload and close the session
|
||||||
|
//
|
||||||
|
// We track how many bytes we uploaded to determine which phase we should be in.
|
||||||
|
var sessionId: String? = null
|
||||||
|
for (i in 0 until CHUNKED_UPLOAD_MAX_ATTEMPTS) {
|
||||||
|
if (i > 0) {
|
||||||
|
Timber.v("Retrying chunked upload (" + (i + 1) + " / " + CHUNKED_UPLOAD_MAX_ATTEMPTS + " attempts)")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// if this is a retry, make sure seek to the correct offset
|
||||||
|
it.skip(uploaded)
|
||||||
|
|
||||||
|
// (1) Start
|
||||||
|
if (sessionId == null) {
|
||||||
|
sessionId = client() //
|
||||||
|
.files() //
|
||||||
|
.uploadSessionStart() //
|
||||||
|
.uploadAndFinish(object : TransferredBytesAwareInputStream(it) {
|
||||||
|
override fun bytesTransferred(transferred: Long) {
|
||||||
|
progressAware.onProgress( //
|
||||||
|
Progress.progress(UploadState.upload(file)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(size) //
|
||||||
|
.withValue(transferred)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, CHUNKED_UPLOAD_CHUNK_SIZE).sessionId
|
||||||
|
uploaded += CHUNKED_UPLOAD_CHUNK_SIZE
|
||||||
|
progressAware.onProgress( //
|
||||||
|
Progress.progress(UploadState.upload(file)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(size) //
|
||||||
|
.withValue(uploaded)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var cursor = UploadSessionCursor(sessionId, uploaded)
|
||||||
|
|
||||||
|
// (2) Append
|
||||||
|
while (size - uploaded > CHUNKED_UPLOAD_CHUNK_SIZE) {
|
||||||
|
val fullyUploaded = uploaded
|
||||||
|
client() //
|
||||||
|
.files() //
|
||||||
|
.uploadSessionAppendV2(cursor) //
|
||||||
|
.uploadAndFinish(object : TransferredBytesAwareInputStream(it) {
|
||||||
|
override fun bytesTransferred(transferred: Long) {
|
||||||
|
progressAware.onProgress( //
|
||||||
|
Progress.progress(UploadState.upload(file)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(size) //
|
||||||
|
.withValue(fullyUploaded + transferred)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, CHUNKED_UPLOAD_CHUNK_SIZE)
|
||||||
|
uploaded += CHUNKED_UPLOAD_CHUNK_SIZE
|
||||||
|
progressAware.onProgress( //
|
||||||
|
Progress.progress(UploadState.upload(file)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(size) //
|
||||||
|
.withValue(uploaded)
|
||||||
|
)
|
||||||
|
cursor = UploadSessionCursor(sessionId, uploaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3) Finish
|
||||||
|
val remaining = size - uploaded
|
||||||
|
val commitInfo = CommitInfo //
|
||||||
|
.newBuilder(file.path) //
|
||||||
|
.withMode(writeMode) //
|
||||||
|
.build()
|
||||||
|
client() //
|
||||||
|
.files() //
|
||||||
|
.uploadSessionFinish(cursor, commitInfo) //
|
||||||
|
.uploadAndFinish(it, remaining)
|
||||||
|
return
|
||||||
|
} catch (ex: RetryException) {
|
||||||
|
thrown = ex
|
||||||
|
// RetryExceptions are never automatically retried by the client for uploads. Must
|
||||||
|
// catch this exception even if DbxRequestConfig.getMaxRetries() > 0.
|
||||||
|
sleepQuietly(ex.backoffMillis)
|
||||||
|
} catch (ex: NetworkIOException) {
|
||||||
|
thrown = ex
|
||||||
|
// Network issue with Dropbox (maybe a timeout?), try again.
|
||||||
|
} catch (ex: UploadSessionLookupErrorException) {
|
||||||
|
if (ex.errorValue.isIncorrectOffset) {
|
||||||
|
thrown = ex
|
||||||
|
// Server offset into the stream doesn't match our offset (uploaded). Seek to
|
||||||
|
// the expected offset according to the server and try again.
|
||||||
|
uploaded = ex.errorValue.incorrectOffsetValue.correctOffset
|
||||||
|
} else {
|
||||||
|
throw FatalBackendException(ex)
|
||||||
|
}
|
||||||
|
} catch (ex: UploadSessionFinishErrorException) {
|
||||||
|
if (ex.errorValue.isLookupFailed && ex.errorValue.lookupFailedValue.isIncorrectOffset) {
|
||||||
|
thrown = ex
|
||||||
|
// Server offset into the stream doesn't match our offset (uploaded). Seek to
|
||||||
|
// the expected offset according to the server and try again.
|
||||||
|
uploaded = ex.errorValue.lookupFailedValue.incorrectOffsetValue.correctOffset
|
||||||
|
} else {
|
||||||
|
throw FatalBackendException(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: throw FatalBackendException("InputStream is null")
|
||||||
|
throw FatalBackendException("Maxed out upload attempts to Dropbox.", thrown)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(DbxException::class, IOException::class)
|
||||||
|
fun read(file: CloudFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
|
||||||
|
progressAware.onProgress(Progress.started(DownloadState.download(file)))
|
||||||
|
var cacheKey: String? = null
|
||||||
|
var cacheFile: File? = null
|
||||||
|
if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) {
|
||||||
|
val fileMetadata = client() //
|
||||||
|
.files() //
|
||||||
|
.getMetadata(file.path) as FileMetadata
|
||||||
|
cacheKey = fileMetadata.id + fileMetadata.rev
|
||||||
|
cacheFile = diskLruCache?.let { it[cacheKey] }
|
||||||
|
}
|
||||||
|
if (sharedPreferencesHandler.useLruCache() && cacheFile != null) {
|
||||||
|
try {
|
||||||
|
retrieveFromLruCache(cacheFile, data)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.tag("DropboxImpl").w(e, "Error while retrieving content from Cache, get from web request")
|
||||||
|
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware)
|
||||||
|
}
|
||||||
|
progressAware.onProgress(Progress.completed(DownloadState.download(file)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(DbxException::class, IOException::class)
|
||||||
|
private fun writeToData(file: CloudFile, data: OutputStream, encryptedTmpFile: File?, cacheKey: String?, progressAware: ProgressAware<DownloadState>) {
|
||||||
|
object : TransferredBytesAwareOutputStream(data) {
|
||||||
|
override fun bytesTransferred(transferred: Long) {
|
||||||
|
progressAware.onProgress( //
|
||||||
|
Progress.progress(DownloadState.download(file)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(file.size ?: Long.MAX_VALUE) //
|
||||||
|
.withValue(transferred)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.use {
|
||||||
|
client() //
|
||||||
|
.files() //
|
||||||
|
.download(file.path) //
|
||||||
|
.download(it)
|
||||||
|
}
|
||||||
|
if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile != null && cacheKey != null) {
|
||||||
|
try {
|
||||||
|
diskLruCache?.let {
|
||||||
|
LruFileCacheUtil.storeToLruCache(it, cacheKey, encryptedTmpFile)
|
||||||
|
} ?: Timber.tag("DropboxImpl").e("Failed to store item in LRU cache")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.tag("DropboxImpl").e(e, "Failed to write downloaded file in LRU cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createLruCache(cacheSize: Int): Boolean {
|
||||||
|
if (diskLruCache == null) {
|
||||||
|
diskLruCache = try {
|
||||||
|
DiskLruCache.create(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.DROPBOX), cacheSize.toLong())
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.tag("DropboxImpl").e(e, "Failed to setup LRU cache")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(AuthenticationException::class, DbxException::class)
|
||||||
|
fun delete(node: CloudNode) {
|
||||||
|
client().files().deleteV2(node.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(AuthenticationException::class, DbxException::class)
|
||||||
|
fun currentAccount(): String {
|
||||||
|
val currentAccount = client().users().currentAccount
|
||||||
|
return currentAccount.name.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val CHUNKED_UPLOAD_CHUNK_SIZE = 8L shl 20
|
||||||
|
private const val CHUNKED_UPLOAD_MAX_ATTEMPTS = 5
|
||||||
|
private fun sleepQuietly(millis: Long) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(millis)
|
||||||
|
} catch (ex: InterruptedException) {
|
||||||
|
throw FatalBackendException("Error uploading to Dropbox: interrupted during backoff.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (cloud.accessToken() == null) {
|
||||||
|
throw NoAuthenticationProvidedException(cloud)
|
||||||
|
}
|
||||||
|
this.cloud = cloud
|
||||||
|
this.root = RootDropboxFolder(cloud)
|
||||||
|
this.context = context
|
||||||
|
sharedPreferencesHandler = SharedPreferencesHandler(context)
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
|
|
||||||
interface DropboxNode extends CloudNode {
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,9 @@
|
|||||||
|
package org.cryptomator.data.cloud.dropbox
|
||||||
|
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
|
||||||
|
interface DropboxNode : CloudNode {
|
||||||
|
|
||||||
|
override val parent: DropboxFolder?
|
||||||
|
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.DropboxCloud;
|
|
||||||
|
|
||||||
class RootDropboxFolder extends DropboxFolder {
|
|
||||||
|
|
||||||
private final DropboxCloud cloud;
|
|
||||||
|
|
||||||
public RootDropboxFolder(DropboxCloud cloud) {
|
|
||||||
super(null, "", "");
|
|
||||||
this.cloud = cloud;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxCloud getCloud() {
|
|
||||||
return cloud;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFolder withCloud(Cloud cloud) {
|
|
||||||
return new RootDropboxFolder((DropboxCloud) cloud);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,11 @@
|
|||||||
|
package org.cryptomator.data.cloud.dropbox
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.DropboxCloud
|
||||||
|
|
||||||
|
internal class RootDropboxFolder(override val cloud: DropboxCloud) : DropboxFolder(null, "", "") {
|
||||||
|
|
||||||
|
override fun withCloud(cloud: Cloud?): DropboxFolder {
|
||||||
|
return RootDropboxFolder(cloud as DropboxCloud)
|
||||||
|
}
|
||||||
|
}
|
@ -1,54 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.file;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
class LocalFile implements CloudFile, LocalNode {
|
|
||||||
|
|
||||||
private final LocalFolder parent;
|
|
||||||
private final String name;
|
|
||||||
private final String path;
|
|
||||||
private final Optional<Long> size;
|
|
||||||
private final Optional<Date> modified;
|
|
||||||
|
|
||||||
LocalFile(LocalFolder parent, String name, String path, Optional<Long> size, Optional<Date> modified) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
this.size = size;
|
|
||||||
this.modified = modified;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return parent.getCloud();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFolder getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Long> getSize() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Date> getModified() {
|
|
||||||
return modified;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,11 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.file
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class LocalFile(override val parent: LocalFolder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, LocalNode {
|
||||||
|
|
||||||
|
override val cloud: Cloud?
|
||||||
|
get() = parent.cloud
|
||||||
|
}
|
@ -1,42 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.file;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
|
|
||||||
class LocalFolder implements CloudFolder, LocalNode {
|
|
||||||
|
|
||||||
private final LocalFolder parent;
|
|
||||||
private final String name;
|
|
||||||
private final String path;
|
|
||||||
|
|
||||||
LocalFolder(LocalFolder parent, String name, String path) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return parent.getCloud();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFolder getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFolder withCloud(Cloud cloud) {
|
|
||||||
return new LocalFolder(parent.withCloud(cloud), name, path);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,14 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.file
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
|
||||||
|
open class LocalFolder(override val parent: LocalFolder?, override val name: String, override val path: String) : CloudFolder, LocalNode {
|
||||||
|
|
||||||
|
override val cloud: Cloud?
|
||||||
|
get() = parent?.cloud
|
||||||
|
|
||||||
|
override fun withCloud(cloud: Cloud?): LocalFolder? {
|
||||||
|
return LocalFolder(parent?.withCloud(cloud), name, path)
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.file;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
|
|
||||||
interface LocalNode extends CloudNode {
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,9 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.file
|
||||||
|
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
|
||||||
|
interface LocalNode : CloudNode {
|
||||||
|
|
||||||
|
override val parent: LocalFolder?
|
||||||
|
|
||||||
|
}
|
@ -1,121 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.file;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
import org.cryptomator.domain.LocalStorageCloud;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
|
||||||
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DownloadState;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.cryptomator.util.ExceptionUtil.contains;
|
|
||||||
|
|
||||||
public class LocalStorageContentRepository implements CloudContentRepository<LocalStorageCloud, LocalNode, LocalFolder, LocalFile> {
|
|
||||||
|
|
||||||
private final LocalStorageImpl localStorageImpl;
|
|
||||||
|
|
||||||
public LocalStorageContentRepository(Context context, LocalStorageCloud localStorageCloud) {
|
|
||||||
this.localStorageImpl = new LocalStorageImpl(context, localStorageCloud);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFolder root(LocalStorageCloud cloud) throws BackendException {
|
|
||||||
return localStorageImpl.root();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFolder resolve(LocalStorageCloud cloud, String path) throws BackendException {
|
|
||||||
return localStorageImpl.resolve(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFile file(LocalFolder parent, String name) throws BackendException {
|
|
||||||
return localStorageImpl.file(parent, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFile file(LocalFolder parent, String name, Optional<Long> size) throws BackendException {
|
|
||||||
return localStorageImpl.file(parent, name, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFolder folder(LocalFolder parent, String name) throws BackendException {
|
|
||||||
return localStorageImpl.folder(parent, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean exists(LocalNode node) throws BackendException {
|
|
||||||
return localStorageImpl.exists(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CloudNode> list(LocalFolder folder) throws BackendException {
|
|
||||||
return localStorageImpl.list(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFolder create(LocalFolder folder) throws BackendException {
|
|
||||||
return localStorageImpl.create(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFolder move(LocalFolder source, LocalFolder target) throws BackendException {
|
|
||||||
return (LocalFolder) localStorageImpl.move(source, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFile move(LocalFile source, LocalFile target) throws BackendException {
|
|
||||||
return (LocalFile) localStorageImpl.move(source, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalFile write(LocalFile file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
|
|
||||||
try {
|
|
||||||
return localStorageImpl.write(file, data, progressAware, replace, size);
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (contains(e, FileNotFoundException.class)) {
|
|
||||||
throw new NoSuchCloudFileException(file.getName());
|
|
||||||
}
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void read(LocalFile file, Optional<File> tmpEncryptedFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
|
||||||
try {
|
|
||||||
localStorageImpl.read(file, data, progressAware);
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (contains(e, FileNotFoundException.class)) {
|
|
||||||
throw new NoSuchCloudFileException(file.getName());
|
|
||||||
}
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(LocalNode node) throws BackendException {
|
|
||||||
localStorageImpl.delete(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String checkAuthenticationAndRetrieveCurrentAccount(LocalStorageCloud cloud) throws BackendException {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void logout(LocalStorageCloud cloud) throws BackendException {
|
|
||||||
// empty
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,111 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.file
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.cryptomator.domain.LocalStorageCloud
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
import org.cryptomator.domain.exception.FatalBackendException
|
||||||
|
import org.cryptomator.domain.exception.NoSuchCloudFileException
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DownloadState
|
||||||
|
import org.cryptomator.domain.usecases.cloud.UploadState
|
||||||
|
import org.cryptomator.util.ExceptionUtil
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
class LocalStorageContentRepository(context: Context, localStorageCloud: LocalStorageCloud) : CloudContentRepository<LocalStorageCloud, LocalNode, LocalFolder, LocalFile> {
|
||||||
|
|
||||||
|
private val localStorageImpl: LocalStorageImpl = LocalStorageImpl(context, localStorageCloud)
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun root(cloud: LocalStorageCloud): LocalFolder {
|
||||||
|
return localStorageImpl.root()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(cloud: LocalStorageCloud, path: String): LocalFolder {
|
||||||
|
return localStorageImpl.resolve(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun file(parent: LocalFolder, name: String): LocalFile {
|
||||||
|
return localStorageImpl.file(parent, name, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun file(parent: LocalFolder, name: String, size: Long?): LocalFile {
|
||||||
|
return localStorageImpl.file(parent, name, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun folder(parent: LocalFolder, name: String): LocalFolder {
|
||||||
|
return localStorageImpl.folder(parent, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun exists(node: LocalNode): Boolean {
|
||||||
|
return localStorageImpl.exists(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun list(folder: LocalFolder): List<LocalNode> {
|
||||||
|
return localStorageImpl.list(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun create(folder: LocalFolder): LocalFolder {
|
||||||
|
return localStorageImpl.create(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: LocalFolder, target: LocalFolder): LocalFolder {
|
||||||
|
return localStorageImpl.move(source, target) as LocalFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: LocalFile, target: LocalFile): LocalFile {
|
||||||
|
return localStorageImpl.move(source, target) as LocalFile
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun write(file: LocalFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): LocalFile {
|
||||||
|
return try {
|
||||||
|
localStorageImpl.write(file, data, progressAware, replace, size)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (ExceptionUtil.contains(e, FileNotFoundException::class.java)) {
|
||||||
|
throw NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun read(file: LocalFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
|
||||||
|
try {
|
||||||
|
localStorageImpl.read(file, data, progressAware)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (ExceptionUtil.contains(e, FileNotFoundException::class.java)) {
|
||||||
|
throw NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun delete(node: LocalNode) {
|
||||||
|
localStorageImpl.delete(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: LocalStorageCloud): String {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun logout(cloud: LocalStorageCloud) {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,193 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.file;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.cryptomator.data.util.TransferredBytesAwareInputStream;
|
|
||||||
import org.cryptomator.data.util.TransferredBytesAwareOutputStream;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
import org.cryptomator.domain.LocalStorageCloud;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
|
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
|
||||||
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DownloadState;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.Progress;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.cryptomator.data.util.CopyStream.copyStreamToStream;
|
|
||||||
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
|
|
||||||
|
|
||||||
class LocalStorageImpl {
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final RootLocalFolder root;
|
|
||||||
|
|
||||||
LocalStorageImpl(Context context, LocalStorageCloud localStorageCloud) {
|
|
||||||
this.context = context;
|
|
||||||
this.root = new RootLocalFolder(localStorageCloud);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalFolder root() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalFolder resolve(String path) {
|
|
||||||
if (path.startsWith(root.getPath())) {
|
|
||||||
path = path.substring(root.getPath().length() + 1);
|
|
||||||
}
|
|
||||||
String[] names = path.split("/");
|
|
||||||
LocalFolder folder = root;
|
|
||||||
for (String name : names) {
|
|
||||||
folder = folder(folder, name);
|
|
||||||
}
|
|
||||||
return folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalFile file(CloudFolder folder, String name) {
|
|
||||||
return file(folder, name, Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalFile file(CloudFolder folder, String name, Optional<Long> size) {
|
|
||||||
return LocalStorageNodeFactory.file( //
|
|
||||||
(LocalFolder) folder, //
|
|
||||||
name, //
|
|
||||||
folder.getPath() + '/' + name, //
|
|
||||||
size, //
|
|
||||||
Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalFolder folder(CloudFolder folder, String name) {
|
|
||||||
return LocalStorageNodeFactory.folder( //
|
|
||||||
(LocalFolder) folder, //
|
|
||||||
name, //
|
|
||||||
folder.getPath() + '/' + name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean exists(CloudNode node) {
|
|
||||||
return new File(node.getPath()).exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<CloudNode> list(LocalFolder folder) throws BackendException {
|
|
||||||
List<CloudNode> result = new ArrayList<>();
|
|
||||||
File localDirectory = new File(folder.getPath());
|
|
||||||
if (!exists(folder)) {
|
|
||||||
throw new NoSuchCloudFileException();
|
|
||||||
}
|
|
||||||
for (File file : localDirectory.listFiles()) {
|
|
||||||
result.add(LocalStorageNodeFactory.from(folder, file));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalFolder create(LocalFolder folder) throws BackendException {
|
|
||||||
File createFolder = new File(folder.getPath());
|
|
||||||
if (createFolder.exists()) {
|
|
||||||
throw new CloudNodeAlreadyExistsException(folder.getName());
|
|
||||||
}
|
|
||||||
if (!createFolder.mkdirs()) {
|
|
||||||
throw new FatalBackendException("Couldn't create a local folder at " + folder.getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
return LocalStorageNodeFactory.folder( //
|
|
||||||
folder.getParent(), //
|
|
||||||
createFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalNode move(CloudNode source, CloudNode target) throws BackendException {
|
|
||||||
File sourceFile = new File(source.getPath());
|
|
||||||
File targetFile = new File(target.getPath());
|
|
||||||
if (targetFile.exists()) {
|
|
||||||
throw new CloudNodeAlreadyExistsException(target.getName());
|
|
||||||
}
|
|
||||||
if (!sourceFile.exists()) {
|
|
||||||
throw new NoSuchCloudFileException(source.getName());
|
|
||||||
}
|
|
||||||
if (!sourceFile.renameTo(targetFile)) {
|
|
||||||
throw new FatalBackendException("Couldn't move " + source.getPath() + " to " + target.getPath());
|
|
||||||
}
|
|
||||||
return LocalStorageNodeFactory.from((LocalFolder) target.getParent(), targetFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete(CloudNode node) {
|
|
||||||
File fileOrDirectory = new File(node.getPath());
|
|
||||||
if (!deleteRecursive(fileOrDirectory)) {
|
|
||||||
throw new FatalBackendException("Couldn't delete local CloudNode " + fileOrDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean deleteRecursive(File fileOrDirectory) {
|
|
||||||
if (fileOrDirectory.isDirectory()) {
|
|
||||||
for (File child : fileOrDirectory.listFiles()) {
|
|
||||||
deleteRecursive(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fileOrDirectory.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalFile write(final CloudFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, final long size) throws IOException, BackendException {
|
|
||||||
if (exists(file) && !replace) {
|
|
||||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
|
||||||
}
|
|
||||||
|
|
||||||
progressAware.onProgress(Progress.started(UploadState.upload(file)));
|
|
||||||
File localFile = new File(file.getPath());
|
|
||||||
|
|
||||||
try (OutputStream out = new FileOutputStream(localFile); TransferredBytesAwareInputStream in = new TransferredBytesAwareInputStream(data.open(context)) {
|
|
||||||
@Override
|
|
||||||
public void bytesTransferred(long transferred) {
|
|
||||||
progressAware.onProgress( //
|
|
||||||
progress(UploadState.upload(file)) //
|
|
||||||
.between(0) //
|
|
||||||
.and(size) //
|
|
||||||
.withValue(transferred));
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
copyStreamToStream(in, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
|
|
||||||
|
|
||||||
return LocalStorageNodeFactory.file( //
|
|
||||||
(LocalFolder) file.getParent(), //
|
|
||||||
file.getName(), //
|
|
||||||
localFile.getPath(), //
|
|
||||||
Optional.of(localFile.length()), //
|
|
||||||
Optional.of(new Date(localFile.lastModified())));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void read(final LocalFile file, OutputStream data, final ProgressAware<DownloadState> progressAware) throws IOException {
|
|
||||||
progressAware.onProgress(Progress.started(DownloadState.download(file)));
|
|
||||||
File localFile = new File(file.getPath());
|
|
||||||
|
|
||||||
try (InputStream in = new FileInputStream(localFile); TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) {
|
|
||||||
@Override
|
|
||||||
public void bytesTransferred(long transferred) {
|
|
||||||
progressAware //
|
|
||||||
.onProgress(progress(DownloadState.download(file)) //
|
|
||||||
.between(0) //
|
|
||||||
.and(localFile.length()) //
|
|
||||||
.withValue(transferred));
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
copyStreamToStream(in, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
progressAware.onProgress(Progress.completed(DownloadState.download(file)));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,165 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.file
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.cryptomator.data.util.CopyStream
|
||||||
|
import org.cryptomator.data.util.TransferredBytesAwareInputStream
|
||||||
|
import org.cryptomator.data.util.TransferredBytesAwareOutputStream
|
||||||
|
import org.cryptomator.domain.CloudNode
|
||||||
|
import org.cryptomator.domain.LocalStorageCloud
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
|
||||||
|
import org.cryptomator.domain.exception.FatalBackendException
|
||||||
|
import org.cryptomator.domain.exception.NoSuchCloudFileException
|
||||||
|
import org.cryptomator.domain.exception.ParentFolderIsNullException
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DownloadState
|
||||||
|
import org.cryptomator.domain.usecases.cloud.Progress
|
||||||
|
import org.cryptomator.domain.usecases.cloud.UploadState
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
internal class LocalStorageImpl(private val context: Context, localStorageCloud: LocalStorageCloud) {
|
||||||
|
|
||||||
|
private val root: RootLocalFolder = RootLocalFolder(localStorageCloud)
|
||||||
|
|
||||||
|
fun root(): LocalFolder {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolve(path: String): LocalFolder {
|
||||||
|
val names = path.substring(root.path.length + 1).split("/").toTypedArray()
|
||||||
|
var folder: LocalFolder = root
|
||||||
|
for (name in names) {
|
||||||
|
folder = folder(folder, name)
|
||||||
|
}
|
||||||
|
return folder
|
||||||
|
}
|
||||||
|
|
||||||
|
fun file(folder: LocalFolder, name: String, size: Long?): LocalFile {
|
||||||
|
return LocalStorageNodeFactory.file(folder, name, folder.path + '/' + name, size, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun folder(folder: LocalFolder, name: String): LocalFolder {
|
||||||
|
return LocalStorageNodeFactory.folder(folder, name, folder.path + '/' + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exists(node: CloudNode): Boolean {
|
||||||
|
return File(node.path).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun list(folder: LocalFolder): List<LocalNode> {
|
||||||
|
val localDirectory = File(folder.path)
|
||||||
|
if (!exists(folder)) {
|
||||||
|
throw NoSuchCloudFileException()
|
||||||
|
}
|
||||||
|
return localDirectory.listFiles()?.map { file -> LocalStorageNodeFactory.from(folder, file) }
|
||||||
|
?: throw FatalBackendException("listFiles() shouldn't return null")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun create(folder: LocalFolder): LocalFolder {
|
||||||
|
folder.parent?.let { parentFolder ->
|
||||||
|
val createFolder = File(folder.path)
|
||||||
|
if (createFolder.exists()) {
|
||||||
|
throw CloudNodeAlreadyExistsException(folder.name)
|
||||||
|
}
|
||||||
|
if (!createFolder.mkdirs()) {
|
||||||
|
throw FatalBackendException("Couldn't create a local folder at " + folder.path)
|
||||||
|
}
|
||||||
|
return LocalStorageNodeFactory.folder(parentFolder, createFolder)
|
||||||
|
} ?: throw ParentFolderIsNullException(folder.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun move(source: LocalNode, target: LocalNode): LocalNode {
|
||||||
|
target.parent?.let {
|
||||||
|
val sourceFile = File(source.path)
|
||||||
|
val targetFile = File(target.path)
|
||||||
|
if (targetFile.exists()) {
|
||||||
|
throw CloudNodeAlreadyExistsException(target.name)
|
||||||
|
}
|
||||||
|
if (!sourceFile.exists()) {
|
||||||
|
throw NoSuchCloudFileException(source.name)
|
||||||
|
}
|
||||||
|
if (!sourceFile.renameTo(targetFile)) {
|
||||||
|
throw FatalBackendException("Couldn't move " + source.path + " to " + target.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return LocalStorageNodeFactory.from(it, targetFile)
|
||||||
|
} ?: throw ParentFolderIsNullException(target.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(node: CloudNode) {
|
||||||
|
val fileOrDirectory = File(node.path)
|
||||||
|
if (!deleteRecursive(fileOrDirectory)) {
|
||||||
|
throw FatalBackendException("Couldn't delete local CloudNode $fileOrDirectory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteRecursive(fileOrDirectory: File): Boolean {
|
||||||
|
if (fileOrDirectory.isDirectory) {
|
||||||
|
fileOrDirectory.listFiles()?.forEach {
|
||||||
|
deleteRecursive(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileOrDirectory.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, BackendException::class)
|
||||||
|
fun write(file: LocalFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): LocalFile {
|
||||||
|
if (!replace && exists(file)) {
|
||||||
|
throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false")
|
||||||
|
}
|
||||||
|
progressAware.onProgress(Progress.started(UploadState.upload(file)))
|
||||||
|
val localFile = File(file.path)
|
||||||
|
FileOutputStream(localFile).use { out ->
|
||||||
|
data.open(context)?.use { inputStream ->
|
||||||
|
object : TransferredBytesAwareInputStream(inputStream) {
|
||||||
|
override fun bytesTransferred(transferred: Long) {
|
||||||
|
progressAware.onProgress( //
|
||||||
|
Progress.progress(UploadState.upload(file)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(size) //
|
||||||
|
.withValue(transferred)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.use { CopyStream.copyStreamToStream(it, out) }
|
||||||
|
} ?: throw FatalBackendException("InputStream shouldn't be null")
|
||||||
|
}
|
||||||
|
progressAware.onProgress(Progress.completed(UploadState.upload(file)))
|
||||||
|
return LocalStorageNodeFactory.file( //
|
||||||
|
file.parent, //
|
||||||
|
file.name, //
|
||||||
|
localFile.path, //
|
||||||
|
localFile.length(), //
|
||||||
|
Date(localFile.lastModified())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun read(file: LocalFile, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
|
||||||
|
progressAware.onProgress(Progress.started(DownloadState.download(file)))
|
||||||
|
val localFile = File(file.path)
|
||||||
|
FileInputStream(localFile).use { inputStream ->
|
||||||
|
object : TransferredBytesAwareOutputStream(data) {
|
||||||
|
override fun bytesTransferred(transferred: Long) {
|
||||||
|
progressAware //
|
||||||
|
.onProgress(
|
||||||
|
Progress.progress(DownloadState.download(file)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(localFile.length()) //
|
||||||
|
.withValue(transferred)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.use { out -> CopyStream.copyStreamToStream(inputStream, out) }
|
||||||
|
}
|
||||||
|
progressAware.onProgress(Progress.completed(DownloadState.download(file)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,34 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.file;
|
|
||||||
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
class LocalStorageNodeFactory {
|
|
||||||
|
|
||||||
public static LocalNode from(LocalFolder parent, File file) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
return folder(parent, file);
|
|
||||||
} else {
|
|
||||||
return file( //
|
|
||||||
parent, //
|
|
||||||
file.getName(), //
|
|
||||||
file.getPath(), //
|
|
||||||
Optional.of(file.length()), //
|
|
||||||
Optional.of(new Date(file.lastModified())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LocalFolder folder(LocalFolder parent, File file) {
|
|
||||||
return folder(parent, file.getName(), file.getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LocalFolder folder(LocalFolder parent, String name, String path) {
|
|
||||||
return new LocalFolder(parent, name, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LocalFile file(LocalFolder folder, String name, String path, Optional<Long> size, Optional<Date> modified) {
|
|
||||||
return new LocalFile(folder, name, path, size, modified);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,36 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.file
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
internal object LocalStorageNodeFactory {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun from(parent: LocalFolder, file: File): LocalNode {
|
||||||
|
return if (file.isDirectory) {
|
||||||
|
folder(parent, file)
|
||||||
|
} else {
|
||||||
|
file( //
|
||||||
|
parent, //
|
||||||
|
file.name, //
|
||||||
|
file.path, //
|
||||||
|
file.length(), //
|
||||||
|
Date(file.lastModified())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun folder(parent: LocalFolder, file: File): LocalFolder {
|
||||||
|
return folder(parent, file.name, file.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun folder(parent: LocalFolder, name: String, path: String): LocalFolder {
|
||||||
|
return LocalFolder(parent, name, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun file(folder: LocalFolder, name: String, path: String, size: Long?, modified: Date?): LocalFile {
|
||||||
|
return LocalFile(folder, name, path, size, modified)
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.file;
|
|
||||||
|
|
||||||
import android.os.Environment;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.LocalStorageCloud;
|
|
||||||
|
|
||||||
public class RootLocalFolder extends LocalFolder {
|
|
||||||
|
|
||||||
private final LocalStorageCloud localStorageCloud;
|
|
||||||
|
|
||||||
public RootLocalFolder(LocalStorageCloud localStorageCloud) {
|
|
||||||
super(null, "", Environment.getExternalStorageDirectory().getPath());
|
|
||||||
this.localStorageCloud = localStorageCloud;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return localStorageCloud;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RootLocalFolder withCloud(Cloud cloud) {
|
|
||||||
return new RootLocalFolder((LocalStorageCloud) cloud);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,15 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.file
|
||||||
|
|
||||||
|
import android.os.Environment
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.LocalStorageCloud
|
||||||
|
|
||||||
|
class RootLocalFolder(private val localStorageCloud: LocalStorageCloud) : LocalFolder(null, "", Environment.getExternalStorageDirectory().path) {
|
||||||
|
|
||||||
|
override val cloud: Cloud
|
||||||
|
get() = localStorageCloud
|
||||||
|
|
||||||
|
override fun withCloud(cloud: Cloud?): RootLocalFolder {
|
||||||
|
return RootLocalFolder(cloud as LocalStorageCloud)
|
||||||
|
}
|
||||||
|
}
|
@ -1,74 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.storageaccessframework;
|
|
||||||
|
|
||||||
import android.util.LruCache;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
|
|
||||||
class DocumentIdCache {
|
|
||||||
|
|
||||||
private final LruCache<String, NodeInfo> cache;
|
|
||||||
|
|
||||||
DocumentIdCache() {
|
|
||||||
cache = new LruCache<>(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public NodeInfo get(String path) {
|
|
||||||
return cache.get(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
<T extends LocalStorageAccessNode> T cache(T value) {
|
|
||||||
add(value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(LocalStorageAccessNode node) {
|
|
||||||
add(node.getPath(), new NodeInfo(node));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void add(String path, NodeInfo info) {
|
|
||||||
cache.put(path, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void remove(LocalStorageAccessNode node) {
|
|
||||||
remove(node.getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void remove(String path) {
|
|
||||||
removeChildren(path);
|
|
||||||
cache.remove(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeChildren(String path) {
|
|
||||||
String prefix = path + '/';
|
|
||||||
for (String key : cache.snapshot().keySet()) {
|
|
||||||
if (key.startsWith(prefix)) {
|
|
||||||
cache.remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class NodeInfo {
|
|
||||||
|
|
||||||
private final String id;
|
|
||||||
private final boolean isFolder;
|
|
||||||
|
|
||||||
private NodeInfo(LocalStorageAccessNode node) {
|
|
||||||
this(node.getDocumentId(), node instanceof CloudFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
NodeInfo(String id, boolean isFolder) {
|
|
||||||
this.id = id;
|
|
||||||
this.isFolder = isFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFolder() {
|
|
||||||
return isFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,51 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||||
|
|
||||||
|
import android.util.LruCache
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
|
||||||
|
internal class DocumentIdCache {
|
||||||
|
|
||||||
|
private val cache: LruCache<String, NodeInfo> = LruCache(1000)
|
||||||
|
|
||||||
|
operator fun get(path: String): NodeInfo? {
|
||||||
|
return cache[path]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : LocalStorageAccessNode> cache(value: T): T {
|
||||||
|
add(value)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(node: LocalStorageAccessNode) {
|
||||||
|
add(node.path, NodeInfo(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun add(path: String, info: NodeInfo) {
|
||||||
|
cache.put(path, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(node: LocalStorageAccessNode) {
|
||||||
|
remove(node.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun remove(path: String) {
|
||||||
|
removeChildren(path)
|
||||||
|
cache.remove(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeChildren(path: String) {
|
||||||
|
val prefix = "$path/"
|
||||||
|
for (key in cache.snapshot().keys) {
|
||||||
|
if (key.startsWith(prefix)) {
|
||||||
|
cache.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class NodeInfo(val id: String?, val isFolder: Boolean) {
|
||||||
|
|
||||||
|
constructor(node: LocalStorageAccessNode) : this(node.documentId, node is CloudFolder)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,96 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.storageaccessframework;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import static android.net.Uri.parse;
|
|
||||||
|
|
||||||
class LocalStorageAccessFile implements CloudFile, LocalStorageAccessNode {
|
|
||||||
|
|
||||||
private final LocalStorageAccessFolder parent;
|
|
||||||
private final String name;
|
|
||||||
private final String path;
|
|
||||||
private final Optional<Long> size;
|
|
||||||
private final Optional<Date> modified;
|
|
||||||
private final String documentId;
|
|
||||||
private final String documentUri;
|
|
||||||
|
|
||||||
LocalStorageAccessFile(LocalStorageAccessFolder parent, String name, String path, Optional<Long> size, Optional<Date> modified, String documentId, String documentUri) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
this.size = size;
|
|
||||||
this.modified = modified;
|
|
||||||
this.documentId = documentId;
|
|
||||||
this.documentUri = documentUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return parent.getCloud();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri getUri() {
|
|
||||||
return parse(documentUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFolder getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDocumentId() {
|
|
||||||
return documentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Long> getSize() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Date> getModified() {
|
|
||||||
return modified;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (obj == this) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (obj == null || getClass() != obj.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return internalEquals((LocalStorageAccessFile) obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean internalEquals(LocalStorageAccessFile o) {
|
|
||||||
return path.equals(o.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
final int prime = 31;
|
|
||||||
int hash = 56127034;
|
|
||||||
hash = hash * prime + path.hashCode();
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,42 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class LocalStorageAccessFile(
|
||||||
|
override val parent: LocalStorageAccessFolder,
|
||||||
|
override val name: String,
|
||||||
|
override val path: String,
|
||||||
|
override val size: Long?,
|
||||||
|
override val modified: Date?,
|
||||||
|
override val documentId: String?,
|
||||||
|
private val documentUri: String?
|
||||||
|
) : CloudFile, LocalStorageAccessNode {
|
||||||
|
|
||||||
|
override val cloud: Cloud?
|
||||||
|
get() = parent.cloud
|
||||||
|
override val uri: Uri
|
||||||
|
get() = Uri.parse(documentUri)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other === this) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return if (other == null || javaClass != other.javaClass) {
|
||||||
|
false
|
||||||
|
} else internalEquals(other as LocalStorageAccessFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun internalEquals(o: LocalStorageAccessFile): Boolean {
|
||||||
|
return path == o.path
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
val prime = 31
|
||||||
|
var hash = 56127034
|
||||||
|
hash = hash * prime + path.hashCode()
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
}
|
@ -1,87 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.storageaccessframework;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
|
|
||||||
import static android.net.Uri.parse;
|
|
||||||
|
|
||||||
class LocalStorageAccessFolder implements CloudFolder, LocalStorageAccessNode {
|
|
||||||
|
|
||||||
private final LocalStorageAccessFolder parent;
|
|
||||||
private final String name;
|
|
||||||
private final String path;
|
|
||||||
private final String documentId;
|
|
||||||
private final String documentUri;
|
|
||||||
|
|
||||||
LocalStorageAccessFolder(LocalStorageAccessFolder parent, String name, String path, String documentId, String documentUri) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
this.documentId = documentId;
|
|
||||||
this.documentUri = documentUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return parent.getCloud();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri getUri() {
|
|
||||||
if (documentUri == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parse(documentUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFolder getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDocumentId() {
|
|
||||||
return documentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (obj == this) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (obj == null || getClass() != obj.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return internalEquals((LocalStorageAccessFolder) obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean internalEquals(LocalStorageAccessFolder o) {
|
|
||||||
return path.equals(o.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
final int prime = 31;
|
|
||||||
int hash = 341797327;
|
|
||||||
hash = hash * prime + path.hashCode();
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFolder withCloud(Cloud cloud) {
|
|
||||||
return new LocalStorageAccessFolder(parent.withCloud(cloud), name, path, documentId, documentUri);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,40 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
|
||||||
|
open class LocalStorageAccessFolder(override val parent: LocalStorageAccessFolder?, override val name: String, override val path: String, override val documentId: String?, private val documentUri: String?) :
|
||||||
|
CloudFolder, LocalStorageAccessNode {
|
||||||
|
|
||||||
|
override val cloud: Cloud?
|
||||||
|
get() = parent?.cloud
|
||||||
|
override val uri: Uri?
|
||||||
|
get() = if (documentUri == null) {
|
||||||
|
null
|
||||||
|
} else Uri.parse(documentUri)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other === this) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return if (other == null || javaClass != other.javaClass) {
|
||||||
|
false
|
||||||
|
} else internalEquals(other as LocalStorageAccessFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun internalEquals(o: LocalStorageAccessFolder): Boolean {
|
||||||
|
return path == o.path
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
val prime = 31
|
||||||
|
var hash = 341797327
|
||||||
|
hash = hash * prime + path.hashCode()
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withCloud(cloud: Cloud?): LocalStorageAccessFolder? {
|
||||||
|
return LocalStorageAccessFolder(parent?.withCloud(cloud), name, path, documentId, documentUri)
|
||||||
|
}
|
||||||
|
}
|
@ -1,123 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.storageaccessframework;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
import org.cryptomator.domain.LocalStorageCloud;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
|
||||||
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DownloadState;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
import org.cryptomator.util.file.MimeTypes;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public class LocalStorageAccessFrameworkContentRepository implements CloudContentRepository<LocalStorageCloud, LocalStorageAccessNode, LocalStorageAccessFolder, LocalStorageAccessFile> {
|
|
||||||
|
|
||||||
private final LocalStorageAccessFrameworkImpl localStorageAccessFramework;
|
|
||||||
|
|
||||||
public LocalStorageAccessFrameworkContentRepository(Context context, MimeTypes mimeTypes, LocalStorageCloud localStorageCloud) {
|
|
||||||
this.localStorageAccessFramework = new LocalStorageAccessFrameworkImpl(context, mimeTypes, localStorageCloud, new DocumentIdCache());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFolder root(LocalStorageCloud cloud) throws BackendException {
|
|
||||||
return localStorageAccessFramework.root();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFolder resolve(LocalStorageCloud cloud, String path) throws BackendException {
|
|
||||||
return localStorageAccessFramework.resolve(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name) throws BackendException {
|
|
||||||
return localStorageAccessFramework.file(parent, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, Optional<Long> size) throws BackendException {
|
|
||||||
return localStorageAccessFramework.file(parent, name, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name) throws BackendException {
|
|
||||||
return localStorageAccessFramework.folder(parent, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean exists(LocalStorageAccessNode node) throws BackendException {
|
|
||||||
return localStorageAccessFramework.exists(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CloudNode> list(LocalStorageAccessFolder folder) throws BackendException {
|
|
||||||
return localStorageAccessFramework.list(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFolder create(LocalStorageAccessFolder folder) throws BackendException {
|
|
||||||
return localStorageAccessFramework.create(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFolder move(LocalStorageAccessFolder source, LocalStorageAccessFolder target) throws BackendException {
|
|
||||||
if (source.getDocumentId() == null) {
|
|
||||||
throw new NoSuchCloudFileException(source.getName());
|
|
||||||
}
|
|
||||||
return (LocalStorageAccessFolder) localStorageAccessFramework.move(source, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFile move(LocalStorageAccessFile source, LocalStorageAccessFile target) throws BackendException {
|
|
||||||
return (LocalStorageAccessFile) localStorageAccessFramework.move(source, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalStorageAccessFile write(LocalStorageAccessFile file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
|
|
||||||
try {
|
|
||||||
return localStorageAccessFramework.write(file, data, progressAware, replace, size);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void read(LocalStorageAccessFile file, Optional<File> tmpEnctypted, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
|
||||||
try {
|
|
||||||
if (file.getDocumentId() == null) {
|
|
||||||
throw new NoSuchCloudFileException(file.getName());
|
|
||||||
}
|
|
||||||
localStorageAccessFramework.read(file, data, progressAware);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(LocalStorageAccessNode node) throws BackendException {
|
|
||||||
localStorageAccessFramework.delete(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String checkAuthenticationAndRetrieveCurrentAccount(LocalStorageCloud cloud) throws BackendException {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void logout(LocalStorageCloud cloud) throws BackendException {
|
|
||||||
// empty
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,111 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.cryptomator.domain.LocalStorageCloud
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
import org.cryptomator.domain.exception.FatalBackendException
|
||||||
|
import org.cryptomator.domain.exception.NoSuchCloudFileException
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DownloadState
|
||||||
|
import org.cryptomator.domain.usecases.cloud.UploadState
|
||||||
|
import org.cryptomator.util.file.MimeTypes
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
class LocalStorageAccessFrameworkContentRepository(context: Context, mimeTypes: MimeTypes, localStorageCloud: LocalStorageCloud) :
|
||||||
|
CloudContentRepository<LocalStorageCloud, LocalStorageAccessNode, LocalStorageAccessFolder, LocalStorageAccessFile> {
|
||||||
|
|
||||||
|
private val localStorageAccessFramework: LocalStorageAccessFrameworkImpl = LocalStorageAccessFrameworkImpl(context, mimeTypes, localStorageCloud, DocumentIdCache())
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun root(cloud: LocalStorageCloud): LocalStorageAccessFolder {
|
||||||
|
return localStorageAccessFramework.root()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(cloud: LocalStorageCloud, path: String): LocalStorageAccessFolder {
|
||||||
|
return localStorageAccessFramework.resolve(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun file(parent: LocalStorageAccessFolder, name: String): LocalStorageAccessFile {
|
||||||
|
return localStorageAccessFramework.file(parent, name, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun file(parent: LocalStorageAccessFolder, name: String, size: Long?): LocalStorageAccessFile {
|
||||||
|
return localStorageAccessFramework.file(parent, name, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun folder(parent: LocalStorageAccessFolder, name: String): LocalStorageAccessFolder {
|
||||||
|
return localStorageAccessFramework.folder(parent, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun exists(node: LocalStorageAccessNode): Boolean {
|
||||||
|
return localStorageAccessFramework.exists(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun list(folder: LocalStorageAccessFolder): List<LocalStorageAccessNode> {
|
||||||
|
return localStorageAccessFramework.list(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun create(folder: LocalStorageAccessFolder): LocalStorageAccessFolder {
|
||||||
|
return localStorageAccessFramework.create(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: LocalStorageAccessFolder, target: LocalStorageAccessFolder): LocalStorageAccessFolder {
|
||||||
|
if (source.documentId == null) {
|
||||||
|
throw NoSuchCloudFileException(source.name)
|
||||||
|
}
|
||||||
|
return localStorageAccessFramework.move(source, target) as LocalStorageAccessFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: LocalStorageAccessFile, target: LocalStorageAccessFile): LocalStorageAccessFile {
|
||||||
|
return localStorageAccessFramework.move(source, target) as LocalStorageAccessFile
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun write(file: LocalStorageAccessFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): LocalStorageAccessFile {
|
||||||
|
return try {
|
||||||
|
localStorageAccessFramework.write(file, data, progressAware, replace, size)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun read(file: LocalStorageAccessFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
|
||||||
|
try {
|
||||||
|
if (file.documentId == null) {
|
||||||
|
throw NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
localStorageAccessFramework.read(file, data, progressAware)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun delete(node: LocalStorageAccessNode) {
|
||||||
|
localStorageAccessFramework.delete(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: LocalStorageCloud): String {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun logout(cloud: LocalStorageCloud) {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,536 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.storageaccessframework;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.UriPermission;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.provider.DocumentsContract;
|
|
||||||
import android.provider.DocumentsContract.Document;
|
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
|
|
||||||
import org.cryptomator.data.util.TransferredBytesAwareInputStream;
|
|
||||||
import org.cryptomator.data.util.TransferredBytesAwareOutputStream;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
import org.cryptomator.domain.LocalStorageCloud;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
|
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
|
||||||
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
|
||||||
import org.cryptomator.domain.exception.NotFoundException;
|
|
||||||
import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DownloadState;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.Progress;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
import org.cryptomator.util.Supplier;
|
|
||||||
import org.cryptomator.util.file.MimeType;
|
|
||||||
import org.cryptomator.util.file.MimeTypes;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.from;
|
|
||||||
import static org.cryptomator.data.util.CopyStream.closeQuietly;
|
|
||||||
import static org.cryptomator.data.util.CopyStream.copyStreamToStream;
|
|
||||||
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
|
||||||
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
class LocalStorageAccessFrameworkImpl {
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final RootLocalStorageAccessFolder root;
|
|
||||||
private final DocumentIdCache idCache;
|
|
||||||
private final MimeTypes mimeTypes;
|
|
||||||
|
|
||||||
LocalStorageAccessFrameworkImpl(Context context, MimeTypes mimeTypes, LocalStorageCloud cloud, DocumentIdCache documentIdCache) {
|
|
||||||
this.mimeTypes = mimeTypes;
|
|
||||||
if (!hasUriPermissions(context, cloud.rootUri())) {
|
|
||||||
throw new NoAuthenticationProvidedException(cloud);
|
|
||||||
}
|
|
||||||
this.context = context;
|
|
||||||
this.root = new RootLocalStorageAccessFolder(cloud);
|
|
||||||
this.idCache = documentIdCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasUriPermissions(Context context, String uri) {
|
|
||||||
Optional<UriPermission> uriPermission = uriPermissionFor(context, uri);
|
|
||||||
return uriPermission.isPresent() //
|
|
||||||
&& uriPermission.get().isReadPermission() //
|
|
||||||
&& uriPermission.get().isWritePermission();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<UriPermission> uriPermissionFor(Context context, String uri) {
|
|
||||||
for (UriPermission uriPermission : context.getContentResolver().getPersistedUriPermissions()) {
|
|
||||||
if (uri.equals(uriPermission.getUri().toString())) {
|
|
||||||
return Optional.of(uriPermission);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalStorageAccessFolder root() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalStorageAccessFolder resolve(String path) throws BackendException {
|
|
||||||
if (path.startsWith("/")) {
|
|
||||||
path = path.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] names = path.split("/");
|
|
||||||
LocalStorageAccessFolder folder = root;
|
|
||||||
for (String name : names) {
|
|
||||||
folder = folder(folder, name);
|
|
||||||
}
|
|
||||||
return folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name) throws BackendException {
|
|
||||||
return file( //
|
|
||||||
parent, //
|
|
||||||
name, //
|
|
||||||
Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, Optional<Long> size) throws BackendException {
|
|
||||||
if (parent.getDocumentId() == null) {
|
|
||||||
return LocalStorageAccessFrameworkNodeFactory.file( //
|
|
||||||
parent, //
|
|
||||||
name, //
|
|
||||||
size);
|
|
||||||
}
|
|
||||||
String path = LocalStorageAccessFrameworkNodeFactory.getNodePath(parent, name);
|
|
||||||
DocumentIdCache.NodeInfo nodeInfo = idCache.get(path);
|
|
||||||
if (nodeInfo != null && !nodeInfo.isFolder()) {
|
|
||||||
return LocalStorageAccessFrameworkNodeFactory.file( //
|
|
||||||
parent, //
|
|
||||||
name, //
|
|
||||||
path, //
|
|
||||||
size, //
|
|
||||||
nodeInfo.getId());
|
|
||||||
}
|
|
||||||
List<LocalStorageAccessNode> cloudNodes = listFilesWithNameFilter(parent, name);
|
|
||||||
if (cloudNodes.size() > 0) {
|
|
||||||
LocalStorageAccessNode cloudNode = cloudNodes.get(0);
|
|
||||||
if (cloudNode instanceof LocalStorageAccessFile) {
|
|
||||||
return idCache.cache((LocalStorageAccessFile) cloudNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return LocalStorageAccessFrameworkNodeFactory.file( //
|
|
||||||
parent, //
|
|
||||||
name, //
|
|
||||||
size);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name) throws BackendException {
|
|
||||||
if (parent.getDocumentId() == null) {
|
|
||||||
return LocalStorageAccessFrameworkNodeFactory.folder( //
|
|
||||||
parent, //
|
|
||||||
name);
|
|
||||||
}
|
|
||||||
String path = LocalStorageAccessFrameworkNodeFactory.getNodePath(parent, name);
|
|
||||||
DocumentIdCache.NodeInfo nodeInfo = idCache.get(path);
|
|
||||||
if (nodeInfo != null && nodeInfo.isFolder()) {
|
|
||||||
return LocalStorageAccessFrameworkNodeFactory.folder( //
|
|
||||||
parent, //
|
|
||||||
name, //
|
|
||||||
nodeInfo.getId());
|
|
||||||
}
|
|
||||||
List<LocalStorageAccessNode> cloudNodes = listFilesWithNameFilter(parent, name);
|
|
||||||
if (cloudNodes.size() > 0) {
|
|
||||||
LocalStorageAccessNode cloudNode = cloudNodes.get(0);
|
|
||||||
if (cloudNode instanceof LocalStorageAccessFolder) {
|
|
||||||
return idCache.cache((LocalStorageAccessFolder) cloudNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return LocalStorageAccessFrameworkNodeFactory.folder( //
|
|
||||||
parent, //
|
|
||||||
name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<LocalStorageAccessNode> listFilesWithNameFilter(LocalStorageAccessFolder parent, String name) throws BackendException {
|
|
||||||
if (parent.getUri() == null) {
|
|
||||||
List<LocalStorageAccessNode> parents = listFilesWithNameFilter(parent.getParent(), parent.getName());
|
|
||||||
if (parents.isEmpty() || !(parents.get(0) instanceof LocalStorageAccessFolder)) {
|
|
||||||
throw new NoSuchCloudFileException(name);
|
|
||||||
}
|
|
||||||
parent = (LocalStorageAccessFolder) parents.get(0);
|
|
||||||
}
|
|
||||||
Cursor childCursor = null;
|
|
||||||
try {
|
|
||||||
childCursor = contentResolver() //
|
|
||||||
.query( //
|
|
||||||
DocumentsContract.buildChildDocumentsUriUsingTree( //
|
|
||||||
parent.getUri(), //
|
|
||||||
parent.getDocumentId()), //
|
|
||||||
new String[] {Document.COLUMN_DISPLAY_NAME, // cursor position 0
|
|
||||||
Document.COLUMN_MIME_TYPE, // cursor position 1
|
|
||||||
Document.COLUMN_SIZE, // cursor position 2
|
|
||||||
Document.COLUMN_LAST_MODIFIED, // cursor position 3
|
|
||||||
Document.COLUMN_DOCUMENT_ID // cursor position 4
|
|
||||||
}, //
|
|
||||||
null, //
|
|
||||||
null, //
|
|
||||||
null);
|
|
||||||
|
|
||||||
List<LocalStorageAccessNode> result = new ArrayList<>();
|
|
||||||
while (childCursor != null && childCursor.moveToNext()) {
|
|
||||||
if (childCursor.getString(0).equals(name)) {
|
|
||||||
result.add(idCache.cache(from(parent, childCursor)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
if (e.getMessage().contains(FileNotFoundException.class.getCanonicalName())) {
|
|
||||||
throw new NoSuchCloudFileException(name);
|
|
||||||
}
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
} finally {
|
|
||||||
closeQuietly(childCursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean exists(LocalStorageAccessNode node) throws BackendException {
|
|
||||||
try {
|
|
||||||
|
|
||||||
List<LocalStorageAccessNode> cloudNodes = listFilesWithNameFilter( //
|
|
||||||
node.getParent(), //
|
|
||||||
node.getName());
|
|
||||||
|
|
||||||
boolean documentExists = cloudNodes.size() > 0;
|
|
||||||
|
|
||||||
if (documentExists) {
|
|
||||||
idCache.add(cloudNodes.get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
return documentExists;
|
|
||||||
} catch (NoSuchCloudFileException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<CloudNode> list(LocalStorageAccessFolder folder) throws BackendException {
|
|
||||||
Cursor childCursor = contentResolver() //
|
|
||||||
.query( //
|
|
||||||
DocumentsContract.buildChildDocumentsUriUsingTree( //
|
|
||||||
folder.getUri(), //
|
|
||||||
folder.getDocumentId()), //
|
|
||||||
new String[] { //
|
|
||||||
Document.COLUMN_DISPLAY_NAME, // cursor position 0
|
|
||||||
Document.COLUMN_MIME_TYPE, // cursor position 1
|
|
||||||
Document.COLUMN_SIZE, // cursor position 2
|
|
||||||
Document.COLUMN_LAST_MODIFIED, // cursor position 3
|
|
||||||
Document.COLUMN_DOCUMENT_ID // cursor position 4
|
|
||||||
}, null, null, null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
List<CloudNode> result = new ArrayList<>();
|
|
||||||
while (childCursor != null && childCursor.moveToNext()) {
|
|
||||||
result.add(idCache.cache(from(folder, childCursor)));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
closeQuietly(childCursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalStorageAccessFolder create(LocalStorageAccessFolder folder) throws BackendException {
|
|
||||||
if (folder //
|
|
||||||
.getParent() //
|
|
||||||
.getDocumentId() == null) {
|
|
||||||
folder = new LocalStorageAccessFolder( //
|
|
||||||
create(folder.getParent()), //
|
|
||||||
folder.getName(), //
|
|
||||||
folder.getPath(), //
|
|
||||||
null, //
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
Uri createdDocument;
|
|
||||||
try {
|
|
||||||
createdDocument = DocumentsContract.createDocument( //
|
|
||||||
contentResolver(), //
|
|
||||||
folder.getParent().getUri(), //
|
|
||||||
Document.MIME_TYPE_DIR, //
|
|
||||||
folder.getName());
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
throw new NoSuchCloudFileException(folder.getName());
|
|
||||||
}
|
|
||||||
return idCache.cache( //
|
|
||||||
LocalStorageAccessFrameworkNodeFactory.folder( //
|
|
||||||
folder.getParent(), //
|
|
||||||
buildDocumentFile(createdDocument)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalStorageAccessNode move(LocalStorageAccessNode source, LocalStorageAccessNode target) throws BackendException {
|
|
||||||
if (exists(target)) {
|
|
||||||
throw new CloudNodeAlreadyExistsException(target.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
idCache.remove(source);
|
|
||||||
idCache.remove(target);
|
|
||||||
boolean isRename = !source //
|
|
||||||
.getName() //
|
|
||||||
.equals(target.getName());
|
|
||||||
boolean isMove = !source //
|
|
||||||
.getParent() //
|
|
||||||
.equals(target.getParent());
|
|
||||||
LocalStorageAccessNode renamedSource = source;
|
|
||||||
if (isRename) {
|
|
||||||
renamedSource = rename(source, target.getName());
|
|
||||||
}
|
|
||||||
if (isMove) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
return idCache.cache( //
|
|
||||||
moveForApiStartingFrom24(renamedSource, target));
|
|
||||||
} else {
|
|
||||||
return idCache.cache( //
|
|
||||||
moveForApiBelow24(renamedSource, target));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return renamedSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocalStorageAccessNode rename(LocalStorageAccessNode source, String name) throws NoSuchCloudFileException {
|
|
||||||
Uri newUri = null;
|
|
||||||
try {
|
|
||||||
newUri = DocumentsContract.renameDocument( //
|
|
||||||
contentResolver(), //
|
|
||||||
source.getUri(), //
|
|
||||||
name);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
// Bug in Android 9 see #460
|
|
||||||
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.P) {
|
|
||||||
throw new NoSuchCloudFileException(source.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bug in Android 9 see #460
|
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
|
|
||||||
try {
|
|
||||||
List<LocalStorageAccessNode> cloudNodes = listFilesWithNameFilter( //
|
|
||||||
source.getParent(), //
|
|
||||||
name);
|
|
||||||
|
|
||||||
newUri = cloudNodes.get(0).getUri();
|
|
||||||
} catch (BackendException e) {
|
|
||||||
Timber.tag("LocalStgeAccessFrkImpl").e(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return LocalStorageAccessFrameworkNodeFactory.from( //
|
|
||||||
source.getParent(), //
|
|
||||||
buildDocumentFile(newUri));
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
private LocalStorageAccessNode moveForApiStartingFrom24(LocalStorageAccessNode source, LocalStorageAccessNode target) throws NoSuchCloudFileException {
|
|
||||||
Uri movedTargetUri;
|
|
||||||
try {
|
|
||||||
movedTargetUri = DocumentsContract.moveDocument( //
|
|
||||||
contentResolver(), //
|
|
||||||
source.getUri(), //
|
|
||||||
source.getParent().getUri(), //
|
|
||||||
target.getParent().getUri());
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
throw new NoSuchCloudFileException(source.getName());
|
|
||||||
}
|
|
||||||
return from( //
|
|
||||||
target.getParent(), //
|
|
||||||
buildDocumentFile(movedTargetUri));
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocalStorageAccessNode moveForApiBelow24(LocalStorageAccessNode source, LocalStorageAccessNode target) throws BackendException {
|
|
||||||
try {
|
|
||||||
LocalStorageAccessNode result;
|
|
||||||
if (source instanceof CloudFolder) {
|
|
||||||
result = moveForApiBelow24( //
|
|
||||||
(LocalStorageAccessFolder) source, //
|
|
||||||
(LocalStorageAccessFolder) target);
|
|
||||||
} else {
|
|
||||||
result = moveForApiBelow24( //
|
|
||||||
(LocalStorageAccessFile) source, //
|
|
||||||
(LocalStorageAccessFile) target);
|
|
||||||
}
|
|
||||||
delete(source);
|
|
||||||
return result;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocalStorageAccessFolder moveForApiBelow24(LocalStorageAccessFolder source, LocalStorageAccessFolder target) throws IOException, BackendException {
|
|
||||||
if (!exists(target.getParent())) {
|
|
||||||
throw new NoSuchCloudFileException(target.getParent().getPath());
|
|
||||||
}
|
|
||||||
LocalStorageAccessFolder createdFolder = create(target);
|
|
||||||
for (CloudNode child : list(source)) {
|
|
||||||
if (child instanceof CloudFolder) {
|
|
||||||
moveForApiBelow24( //
|
|
||||||
(LocalStorageAccessFolder) child, //
|
|
||||||
folder(target, child.getName()));
|
|
||||||
} else {
|
|
||||||
moveForApiBelow24( //
|
|
||||||
(LocalStorageAccessFile) child, //
|
|
||||||
file(target, child.getName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return createdFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocalStorageAccessFile moveForApiBelow24(final LocalStorageAccessFile source, LocalStorageAccessFile target) throws IOException, BackendException {
|
|
||||||
DataSource dataSource = new DataSource() {
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Long> size(Context context) {
|
|
||||||
return source.getSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputStream open(Context context) throws IOException {
|
|
||||||
return contentResolver().openInputStream(source.getUri());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DataSource decorate(DataSource delegate) {
|
|
||||||
return delegate;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return write(target, dataSource, NO_OP_PROGRESS_AWARE, true, source.getSize().get());
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalStorageAccessFile write( //
|
|
||||||
LocalStorageAccessFile file, //
|
|
||||||
final DataSource data, //
|
|
||||||
final ProgressAware<UploadState> progressAware, //
|
|
||||||
final boolean replace, //
|
|
||||||
final long size) throws IOException, BackendException {
|
|
||||||
|
|
||||||
progressAware.onProgress(Progress.started(UploadState.upload(file)));
|
|
||||||
Optional<Uri> fileUri = existingFileUri(file);
|
|
||||||
if (fileUri.isPresent() && !replace) {
|
|
||||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.getParent().getUri() == null) {
|
|
||||||
LocalStorageAccessFolder parent = (LocalStorageAccessFolder) listFilesWithNameFilter(file.getParent().getParent(), file.getParent().getName()).get(0);
|
|
||||||
String tmpFileUri = fileUri.isPresent() ? fileUri.get().toString() : "";
|
|
||||||
file = new LocalStorageAccessFile(parent, file.getName(), file.getPath(), file.getSize(), file.getModified(), file.getDocumentId(), tmpFileUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
final LocalStorageAccessFile tmpFile = file;
|
|
||||||
|
|
||||||
Uri uploadUri = fileUri.orElseGet(createNewDocumentSupplier(tmpFile));
|
|
||||||
if (uploadUri == null) {
|
|
||||||
throw new NotFoundException(tmpFile.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
try (OutputStream out = contentResolver().openOutputStream(uploadUri); //
|
|
||||||
TransferredBytesAwareInputStream in = new TransferredBytesAwareInputStream(data.open(context)) {
|
|
||||||
@Override
|
|
||||||
public void bytesTransferred(long transferred) {
|
|
||||||
progressAware //
|
|
||||||
.onProgress(progress(UploadState.upload(tmpFile)) //
|
|
||||||
.between(0) //
|
|
||||||
.and(size) //
|
|
||||||
.withValue(transferred));
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
if (out instanceof FileOutputStream) {
|
|
||||||
((FileOutputStream) out).getChannel().truncate(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
copyStreamToStream(in, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
|
|
||||||
|
|
||||||
return LocalStorageAccessFrameworkNodeFactory.file( //
|
|
||||||
file.getParent(), //
|
|
||||||
buildDocumentFile(uploadUri));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Supplier<Uri> createNewDocumentSupplier(final LocalStorageAccessFile file) {
|
|
||||||
return () -> {
|
|
||||||
MimeType mimeType = mimeTypes.fromFilename(file.getName()) //
|
|
||||||
.orElse(MimeType.APPLICATION_OCTET_STREAM);
|
|
||||||
try {
|
|
||||||
return DocumentsContract.createDocument( //
|
|
||||||
contentResolver(), //
|
|
||||||
file.getParent().getUri(), //
|
|
||||||
mimeType.toString(), //
|
|
||||||
file.getName());
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<Uri> existingFileUri(LocalStorageAccessFile file) throws BackendException {
|
|
||||||
List<LocalStorageAccessNode> nodes = listFilesWithNameFilter( //
|
|
||||||
file.getParent(), //
|
|
||||||
file.getName());
|
|
||||||
if (nodes.size() > 0) {
|
|
||||||
return Optional.of(nodes.get(0).getUri());
|
|
||||||
} else {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void read(final LocalStorageAccessFile file, final OutputStream data, final ProgressAware<DownloadState> progressAware) throws IOException {
|
|
||||||
progressAware.onProgress(Progress.started(DownloadState.download(file)));
|
|
||||||
|
|
||||||
try (InputStream in = contentResolver().openInputStream(file.getUri()); //
|
|
||||||
TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) {
|
|
||||||
@Override
|
|
||||||
public void bytesTransferred(long transferred) {
|
|
||||||
progressAware.onProgress(progress(DownloadState.download(file)) //
|
|
||||||
.between(0) //
|
|
||||||
.and(file.getSize().orElse(Long.MAX_VALUE)) //
|
|
||||||
.withValue(transferred));
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
copyStreamToStream(in, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
progressAware.onProgress(Progress.completed(DownloadState.download(file)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete(LocalStorageAccessNode node) throws NoSuchCloudFileException {
|
|
||||||
try {
|
|
||||||
DocumentsContract.deleteDocument( //
|
|
||||||
contentResolver(), //
|
|
||||||
node.getUri());
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
throw new NoSuchCloudFileException(node.getName());
|
|
||||||
}
|
|
||||||
idCache.remove(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DocumentFile buildDocumentFile(Uri fileUri) {
|
|
||||||
return DocumentFile.fromSingleUri(context, fileUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ContentResolver contentResolver() {
|
|
||||||
return context.getContentResolver();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,399 @@
|
|||||||
|
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.UriPermission
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.file
|
||||||
|
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.folder
|
||||||
|
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.from
|
||||||
|
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.getNodePath
|
||||||
|
import org.cryptomator.data.util.CopyStream
|
||||||
|
import org.cryptomator.data.util.TransferredBytesAwareInputStream
|
||||||
|
import org.cryptomator.data.util.TransferredBytesAwareOutputStream
|
||||||
|
import org.cryptomator.domain.LocalStorageCloud
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
|
||||||
|
import org.cryptomator.domain.exception.FatalBackendException
|
||||||
|
import org.cryptomator.domain.exception.NoSuchCloudFileException
|
||||||
|
import org.cryptomator.domain.exception.NotFoundException
|
||||||
|
import org.cryptomator.domain.exception.ParentFolderIsNullException
|
||||||
|
import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DownloadState
|
||||||
|
import org.cryptomator.domain.usecases.cloud.Progress
|
||||||
|
import org.cryptomator.domain.usecases.cloud.UploadState
|
||||||
|
import org.cryptomator.util.file.MimeType
|
||||||
|
import org.cryptomator.util.file.MimeTypes
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
|
internal class LocalStorageAccessFrameworkImpl(context: Context, private val mimeTypes: MimeTypes, cloud: LocalStorageCloud, documentIdCache: DocumentIdCache) {
|
||||||
|
|
||||||
|
private val context: Context
|
||||||
|
private val root: RootLocalStorageAccessFolder
|
||||||
|
private val idCache: DocumentIdCache
|
||||||
|
|
||||||
|
private fun hasUriPermissions(context: Context, uri: String): Boolean {
|
||||||
|
val uriPermission = uriPermissionFor(context, uri)
|
||||||
|
return uriPermission != null && uriPermission.isReadPermission && uriPermission.isWritePermission
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun uriPermissionFor(context: Context, uri: String): UriPermission? {
|
||||||
|
return context
|
||||||
|
.contentResolver
|
||||||
|
.persistedUriPermissions
|
||||||
|
.find { uriPermission -> uriPermission.uri.toString() == uri }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun root(): LocalStorageAccessFolder {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun resolve(path: String): LocalStorageAccessFolder {
|
||||||
|
val names = path.removePrefix("/").split("/").toTypedArray()
|
||||||
|
var folder: LocalStorageAccessFolder = root
|
||||||
|
for (name in names) {
|
||||||
|
folder = folder(folder, name)
|
||||||
|
}
|
||||||
|
return folder
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun file(parent: LocalStorageAccessFolder, name: String, size: Long?): LocalStorageAccessFile {
|
||||||
|
if (parent.documentId == null) {
|
||||||
|
return LocalStorageAccessFrameworkNodeFactory.file(parent, name, size)
|
||||||
|
}
|
||||||
|
val path = getNodePath(parent, name)
|
||||||
|
val nodeInfo = idCache[path]
|
||||||
|
if (nodeInfo != null && !nodeInfo.isFolder && nodeInfo.id != null) {
|
||||||
|
return file(parent, name, path, size, nodeInfo.id)
|
||||||
|
}
|
||||||
|
listFilesWithNameFilter(parent, name).getOrNull(0)?.let {
|
||||||
|
if(it is LocalStorageAccessFile) {
|
||||||
|
return idCache.cache(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LocalStorageAccessFrameworkNodeFactory.file(parent, name, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun folder(parent: LocalStorageAccessFolder, name: String): LocalStorageAccessFolder {
|
||||||
|
if (parent.documentId == null) {
|
||||||
|
return LocalStorageAccessFrameworkNodeFactory.folder(parent, name)
|
||||||
|
}
|
||||||
|
val path = getNodePath(parent, name)
|
||||||
|
val nodeInfo = idCache[path]
|
||||||
|
if (nodeInfo != null && nodeInfo.isFolder && nodeInfo.id != null) {
|
||||||
|
return folder(parent, name, nodeInfo.id)
|
||||||
|
}
|
||||||
|
listFilesWithNameFilter(parent, name).getOrNull(0)?.let {
|
||||||
|
if(it is LocalStorageAccessFolder) {
|
||||||
|
return idCache.cache(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LocalStorageAccessFrameworkNodeFactory.folder(parent, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun listFilesWithNameFilter(parent: LocalStorageAccessFolder, name: String): List<LocalStorageAccessNode> {
|
||||||
|
var parent = parent
|
||||||
|
if (parent.uri == null) {
|
||||||
|
parent.parent?.let {
|
||||||
|
val parents = listFilesWithNameFilter(it, parent.name)
|
||||||
|
if (parents.isEmpty() || parents[0] !is LocalStorageAccessFolder) {
|
||||||
|
throw NoSuchCloudFileException(name)
|
||||||
|
}
|
||||||
|
parent = parents[0] as LocalStorageAccessFolder
|
||||||
|
} ?: throw ParentFolderIsNullException(parent.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result: MutableList<LocalStorageAccessNode> = ArrayList()
|
||||||
|
try {
|
||||||
|
contentResolver() //
|
||||||
|
.query( //
|
||||||
|
DocumentsContract.buildChildDocumentsUriUsingTree( //
|
||||||
|
parent.uri, //
|
||||||
|
parent.documentId
|
||||||
|
), arrayOf(
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME, // cursor position 0
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE, // cursor position 1
|
||||||
|
DocumentsContract.Document.COLUMN_SIZE, // cursor position 2
|
||||||
|
DocumentsContract.Document.COLUMN_LAST_MODIFIED, // cursor position 3
|
||||||
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID // cursor position 4
|
||||||
|
), //
|
||||||
|
null, //
|
||||||
|
null, //
|
||||||
|
null
|
||||||
|
)?.use {
|
||||||
|
while (it.moveToNext()) {
|
||||||
|
if (it.getString(0) == name) {
|
||||||
|
result.add(idCache.cache(from(parent, it)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
if (e.message?.contains(FileNotFoundException::class.java.canonicalName!!) == true) {
|
||||||
|
throw NoSuchCloudFileException(name)
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun exists(node: LocalStorageAccessNode): Boolean {
|
||||||
|
node.parent?.let {
|
||||||
|
return try {
|
||||||
|
return listFilesWithNameFilter(it, node.name).getOrNull(0)?.also {
|
||||||
|
idCache.add(it)
|
||||||
|
} != null
|
||||||
|
} catch (e: NoSuchCloudFileException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} ?: throw ParentFolderIsNullException(node.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun list(folder: LocalStorageAccessFolder): List<LocalStorageAccessNode> {
|
||||||
|
val result: MutableList<LocalStorageAccessNode> = ArrayList()
|
||||||
|
contentResolver() //
|
||||||
|
.query( //
|
||||||
|
DocumentsContract.buildChildDocumentsUriUsingTree( //
|
||||||
|
folder.uri, //
|
||||||
|
folder.documentId
|
||||||
|
), arrayOf( //
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME, // cursor position 0
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE, // cursor position 1
|
||||||
|
DocumentsContract.Document.COLUMN_SIZE, // cursor position 2
|
||||||
|
DocumentsContract.Document.COLUMN_LAST_MODIFIED, // cursor position 3
|
||||||
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID // cursor position 4
|
||||||
|
), null, null, null
|
||||||
|
)?.use {
|
||||||
|
while (it.moveToNext()) {
|
||||||
|
result.add(idCache.cache(from(folder, it)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun create(folder: LocalStorageAccessFolder): LocalStorageAccessFolder {
|
||||||
|
var folder = folder
|
||||||
|
folder.parent?.let { foldersParent ->
|
||||||
|
if (foldersParent.documentId == null) {
|
||||||
|
folder = LocalStorageAccessFolder( //
|
||||||
|
create(foldersParent),
|
||||||
|
folder.name, //
|
||||||
|
folder.path, //
|
||||||
|
null, //
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: throw ParentFolderIsNullException(folder.name)
|
||||||
|
|
||||||
|
folder.parent?.let { foldersParent ->
|
||||||
|
foldersParent.uri?.let { foldersParentUri ->
|
||||||
|
val createdDocument = try {
|
||||||
|
DocumentsContract.createDocument( //
|
||||||
|
contentResolver(), //
|
||||||
|
foldersParentUri,
|
||||||
|
DocumentsContract.Document.MIME_TYPE_DIR, //
|
||||||
|
folder.name
|
||||||
|
)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
throw NoSuchCloudFileException(folder.name)
|
||||||
|
} ?: throw FatalBackendException("Failed to create document for unknown reason")
|
||||||
|
|
||||||
|
return idCache.cache(folder(foldersParent, buildDocumentFile(createdDocument)))
|
||||||
|
} ?: throw FatalBackendException("FoldersParentsUri shouldn't be null")
|
||||||
|
} ?: throw ParentFolderIsNullException(folder.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
fun move(source: LocalStorageAccessNode, target: LocalStorageAccessNode): LocalStorageAccessNode {
|
||||||
|
source.parent?.let { sourcesParent ->
|
||||||
|
if (exists(target)) {
|
||||||
|
throw CloudNodeAlreadyExistsException(target.name)
|
||||||
|
}
|
||||||
|
idCache.remove(source)
|
||||||
|
idCache.remove(target)
|
||||||
|
val isRename = source.name != target.name
|
||||||
|
val isMove = sourcesParent != target.parent
|
||||||
|
var renamedSource = source
|
||||||
|
if (isRename) {
|
||||||
|
renamedSource = rename(source, target.name)
|
||||||
|
}
|
||||||
|
return if (isMove) {
|
||||||
|
idCache.cache(internalMove(renamedSource, target))
|
||||||
|
} else renamedSource
|
||||||
|
} ?: throw ParentFolderIsNullException(source.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(NoSuchCloudFileException::class)
|
||||||
|
private fun rename(source: LocalStorageAccessNode, name: String): LocalStorageAccessNode {
|
||||||
|
source.parent?.let { parent ->
|
||||||
|
var newUri = try {
|
||||||
|
DocumentsContract.renameDocument(contentResolver(), source.uri, name)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
/* Bug in Android 9 see #460 TLDR; In this renameDocument-method, Android 9 throws
|
||||||
|
a `FileNotFoundException` although the file exists and is also renamed. */
|
||||||
|
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.P) {
|
||||||
|
throw NoSuchCloudFileException(source.name)
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bug in Android 9 see #460 TLDR; In this renameDocument-method, Android 9 throws
|
||||||
|
a `FileNotFoundException` although the file exists and is also renamed. */
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
|
||||||
|
newUri = try {
|
||||||
|
listFilesWithNameFilter(parent, name).getOrNull(0)?.uri
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
throw FatalBackendException("Failed to list file while move of ${source.name}", e)
|
||||||
|
} ?: throw FatalBackendException("Failed to list file while move of ${source.name} for unkown reason")
|
||||||
|
}
|
||||||
|
|
||||||
|
requireNotNull(newUri)
|
||||||
|
|
||||||
|
return from(parent, buildDocumentFile(newUri))
|
||||||
|
} ?: throw ParentFolderIsNullException(source.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(NoSuchCloudFileException::class)
|
||||||
|
private fun internalMove(source: LocalStorageAccessNode, target: LocalStorageAccessNode): LocalStorageAccessNode {
|
||||||
|
source.uri?.let { sourceUri ->
|
||||||
|
source.parent?.uri?.let { sourcesParentUri ->
|
||||||
|
target.parent?.let { targetsParent ->
|
||||||
|
target.parent?.uri?.let { targetsParentUri ->
|
||||||
|
val movedTargetUri = try {
|
||||||
|
DocumentsContract.moveDocument(contentResolver(), sourceUri, sourcesParentUri, targetsParentUri)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
throw NoSuchCloudFileException(source.name)
|
||||||
|
} ?: throw FatalBackendException("Move failed for unknown reason")
|
||||||
|
return from(targetsParent, buildDocumentFile(movedTargetUri))
|
||||||
|
} ?: throw FatalBackendException("Target parents uri shouldn't be null")
|
||||||
|
} ?: throw FatalBackendException("Targets parent shouldn't be null")
|
||||||
|
} ?: throw FatalBackendException("Source parents uri shouldn't be null")
|
||||||
|
} ?: throw FatalBackendException("Source uri shouldn't be null")
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, BackendException::class)
|
||||||
|
fun write(file: LocalStorageAccessFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): LocalStorageAccessFile {
|
||||||
|
var file = file
|
||||||
|
progressAware.onProgress(Progress.started(UploadState.upload(file)))
|
||||||
|
val fileUri = existingFileUri(file)
|
||||||
|
|
||||||
|
if (!replace && fileUri != null) {
|
||||||
|
throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.parent.uri == null) {
|
||||||
|
file.parent.parent?.let {
|
||||||
|
val parent = listFilesWithNameFilter(it, file.parent.name)[0] as LocalStorageAccessFolder
|
||||||
|
val tmpFileUri = fileUri?.toString() ?: ""
|
||||||
|
file = LocalStorageAccessFile(parent, file.name, file.path, file.size, file.modified, file.documentId, tmpFileUri)
|
||||||
|
} ?: throw ParentFolderIsNullException(file.parent.name)
|
||||||
|
}
|
||||||
|
val tmpFile = file
|
||||||
|
val uploadUri: Uri = (fileUri ?: createNewDocumentSupplier(tmpFile).get()) ?: throw NotFoundException(tmpFile.name)
|
||||||
|
|
||||||
|
data.open(context)?.use { inputStream ->
|
||||||
|
contentResolver().openOutputStream(uploadUri)?.use { out ->
|
||||||
|
object : TransferredBytesAwareInputStream(inputStream) {
|
||||||
|
override fun bytesTransferred(transferred: Long) {
|
||||||
|
progressAware //
|
||||||
|
.onProgress(
|
||||||
|
Progress.progress(UploadState.upload(tmpFile)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(size) //
|
||||||
|
.withValue(transferred)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.use { inputStream ->
|
||||||
|
if (out is FileOutputStream) {
|
||||||
|
out.channel.truncate(0)
|
||||||
|
}
|
||||||
|
CopyStream.copyStreamToStream(inputStream, out)
|
||||||
|
}
|
||||||
|
} ?: throw FatalBackendException("OutputStream shouldn't bee null")
|
||||||
|
} ?: throw FatalBackendException("InputStream shouldn't bee null")
|
||||||
|
|
||||||
|
progressAware.onProgress(Progress.completed(UploadState.upload(file)))
|
||||||
|
return file(file.parent, buildDocumentFile(uploadUri))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNewDocumentSupplier(file: LocalStorageAccessFile): Supplier<Uri?> {
|
||||||
|
return Supplier {
|
||||||
|
val mimeType = if (mimeTypes.fromFilename(file.name) == null) MimeType.APPLICATION_OCTET_STREAM else mimeTypes.fromFilename(file.name)
|
||||||
|
try {
|
||||||
|
DocumentsContract.createDocument(contentResolver(), file.parent.uri, mimeType.toString(), file.name) // FIXME
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
private fun existingFileUri(file: LocalStorageAccessFile): Uri? {
|
||||||
|
return listFilesWithNameFilter(file.parent, file.name).getOrNull(0)?.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun read(file: LocalStorageAccessFile, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
|
||||||
|
progressAware.onProgress(Progress.started(DownloadState.download(file)))
|
||||||
|
contentResolver().openInputStream(file.uri)?.use { inputStream ->
|
||||||
|
object : TransferredBytesAwareOutputStream(data) {
|
||||||
|
override fun bytesTransferred(transferred: Long) {
|
||||||
|
progressAware.onProgress(
|
||||||
|
Progress.progress(DownloadState.download(file)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(file.size ?: Long.MAX_VALUE) //
|
||||||
|
.withValue(transferred)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.use { out -> CopyStream.copyStreamToStream(inputStream, out) }
|
||||||
|
} ?: throw FatalBackendException("InputStream shouldn't bee null")
|
||||||
|
progressAware.onProgress(Progress.completed(DownloadState.download(file)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(NoSuchCloudFileException::class)
|
||||||
|
fun delete(node: LocalStorageAccessNode) {
|
||||||
|
requireNotNull(node.uri)
|
||||||
|
try {
|
||||||
|
DocumentsContract.deleteDocument(contentResolver(), node.uri)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
throw NoSuchCloudFileException(node.name)
|
||||||
|
}
|
||||||
|
idCache.remove(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDocumentFile(fileUri: Uri): DocumentFile {
|
||||||
|
// can only be zero on devices with pre-Kitkat, which is excluded by the minSDK
|
||||||
|
return DocumentFile.fromSingleUri(context, fileUri)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun contentResolver(): ContentResolver {
|
||||||
|
return context.contentResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (!hasUriPermissions(context, cloud.rootUri())) {
|
||||||
|
throw NoAuthenticationProvidedException(cloud)
|
||||||
|
}
|
||||||
|
this.context = context
|
||||||
|
this.root = RootLocalStorageAccessFolder(cloud)
|
||||||
|
idCache = documentIdCache
|
||||||
|
}
|
||||||
|
}
|
@ -1,124 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.local.storageaccessframework;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.provider.DocumentsContract;
|
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
class LocalStorageAccessFrameworkNodeFactory {
|
|
||||||
|
|
||||||
public static LocalStorageAccessNode from(LocalStorageAccessFolder parent, Cursor cursor) {
|
|
||||||
if (isFolder(cursor)) {
|
|
||||||
return folder(parent, cursor);
|
|
||||||
} else {
|
|
||||||
return file(parent, cursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocalStorageAccessFile file(LocalStorageAccessFolder parent, Cursor cursor) {
|
|
||||||
return new LocalStorageAccessFile( //
|
|
||||||
parent, //
|
|
||||||
cursor.getString(0), //
|
|
||||||
getNodePath(parent, cursor.getString(0)), //
|
|
||||||
Optional.of(cursor.getLong(2)), //
|
|
||||||
Optional.of(new Date(cursor.getLong(3))), //
|
|
||||||
cursor.getString(4), //
|
|
||||||
getDocumentUri(parent, cursor.getString(4)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, Cursor cursor) {
|
|
||||||
return new LocalStorageAccessFolder(parent, //
|
|
||||||
cursor.getString(0), //
|
|
||||||
getNodePath(parent, cursor.getString(0)), //
|
|
||||||
cursor.getString(4), //
|
|
||||||
getDocumentUri(parent, cursor.getString(4)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LocalStorageAccessNode from(LocalStorageAccessFolder parent, DocumentFile documentFile) {
|
|
||||||
if (isFolder(documentFile)) {
|
|
||||||
return folder(parent, documentFile);
|
|
||||||
} else {
|
|
||||||
return file(parent, documentFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, DocumentFile directory) {
|
|
||||||
return new LocalStorageAccessFolder(parent, //
|
|
||||||
directory.getName(), //
|
|
||||||
getNodePath(parent, directory.getName()), //
|
|
||||||
DocumentsContract.getDocumentId(directory.getUri()), //
|
|
||||||
directory.getUri().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LocalStorageAccessFile file(LocalStorageAccessFolder parent, DocumentFile documentFile) {
|
|
||||||
return new LocalStorageAccessFile( //
|
|
||||||
parent, //
|
|
||||||
documentFile.getName(), //
|
|
||||||
getNodePath(parent, documentFile.getName()), //
|
|
||||||
Optional.of(documentFile.length()), //
|
|
||||||
Optional.of(new Date(documentFile.lastModified())), //
|
|
||||||
DocumentsContract.getTreeDocumentId(documentFile.getUri()), //
|
|
||||||
documentFile.getUri().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, Optional<Long> size) {
|
|
||||||
return new LocalStorageAccessFile(//
|
|
||||||
parent, //
|
|
||||||
name, //
|
|
||||||
getNodePath(parent, name), //
|
|
||||||
size, //
|
|
||||||
Optional.empty(), //
|
|
||||||
null, //
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, String path, Optional<Long> size, String documentId) {
|
|
||||||
return new LocalStorageAccessFile(parent, //
|
|
||||||
name, //
|
|
||||||
path, //
|
|
||||||
size, //
|
|
||||||
Optional.empty(), //
|
|
||||||
documentId, //
|
|
||||||
getDocumentUri(parent, documentId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name) {
|
|
||||||
return new LocalStorageAccessFolder(parent, //
|
|
||||||
name, //
|
|
||||||
getNodePath(parent, name), //
|
|
||||||
null, //
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name, String documentId) {
|
|
||||||
return new LocalStorageAccessFolder(parent, //
|
|
||||||
name, //
|
|
||||||
getNodePath(parent, name), //
|
|
||||||
documentId, //
|
|
||||||
getDocumentUri(parent, documentId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getDocumentUri(LocalStorageAccessFolder parent, String documentId) {
|
|
||||||
return DocumentsContract.buildDocumentUriUsingTree(parent.getUri(), documentId).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isFolder(DocumentFile file) {
|
|
||||||
return file.isDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isFolder(Cursor cursor) {
|
|
||||||
return cursor.getString(1).equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getNodePath(LocalStorageAccessFolder parent, String name) {
|
|
||||||
return parent.getPath() + "/" + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user