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]'
|
||||
files:
|
||||
- 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
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
java-version: 11
|
||||
- name: Build and Test
|
||||
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
|
||||
**/fastlane/repo/**
|
||||
**/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"]
|
||||
path = msa-auth-for-android
|
||||
path = lib/msa-auth-for-android
|
||||
url = https://github.com/SailReal/msa-auth-for-android.git
|
||||
[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
|
||||
[submodule "pcloud-sdk-java"]
|
||||
path = pcloud-sdk-java
|
||||
url = https://github.com/SailReal/pcloud-sdk-java
|
||||
path = lib/pcloud-sdk-java
|
||||
url = https://github.com/SailReal/pcloud-sdk-java.git
|
||||
|
4
.idea/codeStyles/Project.xml
generated
4
.idea/codeStyles/Project.xml
generated
@ -31,6 +31,7 @@
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="999" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="999" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<Properties>
|
||||
<option name="KEEP_BLANK_LINES" value="true" />
|
||||
@ -178,10 +179,11 @@
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
</component>
|
||||
|
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -45,7 +45,7 @@
|
||||
</value>
|
||||
</option>
|
||||
</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" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
1
.idea/runConfigurations.xml
generated
1
.idea/runConfigurations.xml
generated
@ -3,6 +3,7 @@
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<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.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
|
8
.idea/vcs.xml
generated
8
.idea/vcs.xml
generated
@ -2,8 +2,8 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/msa-auth-for-android" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/pcloud-sdk-java" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/subsampling-scale-image-view" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/lib/msa-auth-for-android" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/lib/pcloud-sdk-java" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/lib/subsampling-scale-image-view" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
150
Gemfile.lock
150
Gemfile.lock
@ -1,65 +1,80 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.3)
|
||||
addressable (2.7.0)
|
||||
CFPropertyList (3.0.4)
|
||||
rexml
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
apktools (0.7.4)
|
||||
rubyzip (~> 2.0)
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.1.1)
|
||||
aws-partitions (1.444.0)
|
||||
aws-sdk-core (3.114.0)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.509.0)
|
||||
aws-sdk-core (3.121.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.43.0)
|
||||
aws-sdk-core (~> 3, >= 3.112.0)
|
||||
aws-sdk-kms (1.48.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.93.1)
|
||||
aws-sdk-core (~> 3, >= 3.112.0)
|
||||
aws-sdk-s3 (1.103.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.3)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
bcrypt_pbkdf (1.0.1)
|
||||
bcrypt_pbkdf (1.1.0)
|
||||
claide (1.0.3)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander-fastlane (4.4.6)
|
||||
highline (~> 1.7.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.3)
|
||||
digest-crc (0.6.4)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.6)
|
||||
ed25519 (1.2.4)
|
||||
emoji_regex (3.2.2)
|
||||
excon (0.80.0)
|
||||
faraday (1.3.0)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.86.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_persistent (~> 1.1)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ruby2_keywords
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday (>= 0.8.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_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)
|
||||
fastimage (2.2.3)
|
||||
fastlane (2.180.1)
|
||||
fastimage (2.2.5)
|
||||
fastlane (2.195.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.3, < 3.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander-fastlane (>= 4.4.6, < 5.0.0)
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
@ -68,19 +83,20 @@ GEM
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-api-client (>= 0.37.0, < 0.39.0)
|
||||
google-cloud-storage (>= 1.15.0, < 2.0.0)
|
||||
highline (>= 1.7.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (~> 2.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
slack-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
@ -89,79 +105,75 @@ GEM
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
fastlane-plugin-aws_s3 (2.0.2)
|
||||
fastlane-plugin-aws_s3 (2.0.3)
|
||||
apktools (~> 0.7)
|
||||
aws-sdk-s3 (~> 1)
|
||||
mime-types (~> 3.3)
|
||||
fastlane-plugin-get_version_name (0.2.2)
|
||||
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)
|
||||
googleauth (~> 0.9)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
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)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
signet (~> 0.14)
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.3.0)
|
||||
google-apis-core (~> 0.1)
|
||||
google-apis-storage_v1 (0.3.0)
|
||||
google-apis-core (~> 0.1)
|
||||
google-apis-iamcredentials_v1 (0.7.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.5.0)
|
||||
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-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.1.0)
|
||||
google-cloud-storage (1.31.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.34.1)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.1)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (0.16.1)
|
||||
googleauth (1.0.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.14)
|
||||
highline (1.7.10)
|
||||
http-cookie (1.0.3)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.4)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.4.0)
|
||||
json (2.5.1)
|
||||
jwt (2.2.2)
|
||||
jwt (2.2.3)
|
||||
memoist (0.16.2)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2021.0225)
|
||||
mime-types-data (3.2021.0704)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.0)
|
||||
mini_mime (1.1.1)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
net-sftp (2.1.2)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (5.2.0)
|
||||
net-sftp (3.0.0)
|
||||
net-ssh (>= 5.0.0, < 7.0.0)
|
||||
net-ssh (6.1.0)
|
||||
optparse (0.1.1)
|
||||
os (1.1.1)
|
||||
plist (3.6.0)
|
||||
public_suffix (4.0.6)
|
||||
rake (13.0.3)
|
||||
rake (13.0.6)
|
||||
representable (3.1.1)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
@ -169,18 +181,17 @@ GEM
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.4)
|
||||
rubyzip (2.3.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.3)
|
||||
signet (0.15.0)
|
||||
addressable (~> 2.3)
|
||||
signet (0.16.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
CFPropertyList
|
||||
naturally
|
||||
slack-notifier (2.3.2)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
@ -192,16 +203,17 @@ GEM
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
unf_ext (0.0.8)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.19.0)
|
||||
xcodeproj (1.21.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
|
41
README.md
41
README.md
@ -3,7 +3,7 @@
|
||||
[](http://twitter.com/Cryptomator)
|
||||
[](https://community.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.
|
||||
|
||||
@ -19,7 +19,7 @@ Cryptomator for Android is currently available in the following distribution ch
|
||||
### Dependencies
|
||||
|
||||
* Git
|
||||
* JDK 8
|
||||
* JDK 11
|
||||
* 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).
|
||||
|
||||
## 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
|
||||
|
||||
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 plugin: "com.vanniktech.android.junit.jacoco"
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.4.32'
|
||||
ext.kotlin_version = '1.5.31'
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
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 'com.fernandocejas.frodo:frodo-plugin:0.8.3'
|
||||
classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.7.1.1"
|
||||
@ -42,7 +39,7 @@ allprojects {
|
||||
ext {
|
||||
androidApplicationId = 'org.cryptomator'
|
||||
androidVersionCode = getVersionCode()
|
||||
androidVersionName = '1.5.18'
|
||||
androidVersionName = '1.6.0'
|
||||
}
|
||||
repositories {
|
||||
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 {
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
androidBuildToolsVersion = "29.0.2"
|
||||
androidMinSdkVersion = 23
|
||||
androidBuildToolsVersion = "30.0.2"
|
||||
androidMinSdkVersion = 24
|
||||
androidTargetSdkVersion = 29
|
||||
androidCompileSdkVersion = 29
|
||||
|
||||
@ -17,8 +18,10 @@ ext {
|
||||
|
||||
// support lib
|
||||
androidSupportAnnotationsVersion = '1.2.0'
|
||||
androidSupportAppcompatVersion = '1.2.0'
|
||||
androidSupportDesignVersion = '1.3.0'
|
||||
androidSupportAppcompatVersion = '1.3.1'
|
||||
androidSupportDesignVersion = '1.4.0'
|
||||
|
||||
coreDesugaringVersion = '1.1.5'
|
||||
|
||||
// app frameworks and utilities
|
||||
|
||||
@ -26,70 +29,73 @@ ext {
|
||||
rxAndroidVersion = '2.1.1'
|
||||
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'
|
||||
|
||||
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'
|
||||
|
||||
lruFileCacheVersion = '1.0'
|
||||
lruFileCacheVersion = '1.2'
|
||||
|
||||
// KEEP IN SYNC WITH GENERATOR VERSION IN root build.gradle
|
||||
greenDaoVersion = '3.3.0'
|
||||
|
||||
// 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
|
||||
cryptolibVersion = '1.3.0'
|
||||
dropboxVersion = '4.0.1'
|
||||
|
||||
awsAndroidSdkS3 = '2.23.0'
|
||||
|
||||
dropboxVersion = '4.0.0'
|
||||
|
||||
googleApiServicesVersion = 'v3-rev197-1.25.0'
|
||||
googlePlayServicesVersion = '19.0.0'
|
||||
googleClientVersion = '1.31.4'
|
||||
googleApiServicesVersion = 'v3-rev20210919-1.32.1'
|
||||
googlePlayServicesVersion = '19.2.0'
|
||||
googleClientVersion = '1.32.1' // keep in sync with https://github.com/SailReal/google-http-java-client
|
||||
/*
|
||||
update using https://github.com/SailReal/google-http-java-client with `mvn clean install`,
|
||||
copying `google-http-client-*.jar` and `google-http-client-android-*.jar` into the lib folder of this project
|
||||
*/
|
||||
trackingFreeGoogleCLientVersion = '1.40.1'
|
||||
|
||||
msgraphVersion = '2.10.0'
|
||||
|
||||
minIoVersion = '8.3.0'
|
||||
staxVersion = '1.2.0' // needed for minIO
|
||||
|
||||
commonsCodecVersion = '1.15'
|
||||
|
||||
recyclerViewFastScrollVersion = '2.0.1'
|
||||
|
||||
// testing dependencies
|
||||
|
||||
jUnitVersion = '5.7.1'
|
||||
jUnit4Version = '4.13.1'
|
||||
jUnitVersion = '5.8.1'
|
||||
assertJVersion = '1.7.1'
|
||||
mockitoVersion = '3.9.0'
|
||||
mockitoInlineVersion = '3.9.0'
|
||||
mockitoVersion = '3.12.4'
|
||||
mockitoKotlinVersion = '3.2.0'
|
||||
hamcrestVersion = '1.3'
|
||||
dexmakerVersion = '1.0'
|
||||
espressoVersion = '3.3.0'
|
||||
espressoVersion = '3.4.0'
|
||||
testingSupportLibVersion = '0.1'
|
||||
runnerVersion = '1.3.0'
|
||||
rulesVersion = '1.3.0'
|
||||
contributionVersion = '3.3.0'
|
||||
runnerVersion = '1.4.0'
|
||||
rulesVersion = '1.4.0'
|
||||
contributionVersion = '3.4.0'
|
||||
uiautomatorVersion = '2.2.0'
|
||||
|
||||
androidxCoreVersion = '1.3.2'
|
||||
androidxFragmentVersion = '1.3.2'
|
||||
androidxCoreVersion = '1.6.0'
|
||||
androidxFragmentVersion = '1.3.6'
|
||||
androidxViewpagerVersion = '1.0.0'
|
||||
androidxSwiperefreshVersion = '1.1.0'
|
||||
androidxPreferenceVersion = '1.0.0' // 1.1.0 and 1.1.2 does have a bug with the text size
|
||||
androidxRecyclerViewVersion = '1.2.0'
|
||||
androidxPreferenceVersion = '1.1.1'
|
||||
androidxRecyclerViewVersion = '1.2.1'
|
||||
androidxDocumentfileVersion = '1.0.1'
|
||||
androidxBiometricVersion = '1.1.0'
|
||||
androidxTestCoreVersion = '1.3.0'
|
||||
androidxTestCoreVersion = '1.4.0'
|
||||
|
||||
jsonWebTokenApiVersion = '0.11.2'
|
||||
|
||||
@ -103,7 +109,6 @@ ext {
|
||||
androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}",
|
||||
androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}",
|
||||
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
|
||||
awsAndroidS3 : "com.amazonaws:aws-android-sdk-s3:${awsAndroidSdkS3}",
|
||||
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
|
||||
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
|
||||
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",
|
||||
@ -112,11 +117,14 @@ ext {
|
||||
dagger : "com.google.dagger:dagger:${daggerVersion}",
|
||||
daggerCompiler : "com.google.dagger:dagger-compiler:${daggerVersion}",
|
||||
design : "com.google.android.material:material:${androidSupportDesignVersion}",
|
||||
coreDesugaring : "com.android.tools:desugar_jdk_libs:${coreDesugaringVersion}",
|
||||
dropbox : "com.dropbox.core:dropbox-core-sdk:${dropboxVersion}",
|
||||
espresso : "androidx.test.espresso:espresso-core:${espressoVersion}",
|
||||
googleApiClientAndroid: "com.google.api-client:google-api-client-android:${googleClientVersion}",
|
||||
googleApiServicesDrive: "com.google.apis:google-api-services-drive:${googleApiServicesVersion}",
|
||||
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}",
|
||||
gson : "com.google.code.gson:gson:${gsonVersion}",
|
||||
hamcrest : "org.hamcrest:hamcrest-all:${hamcrestVersion}",
|
||||
@ -125,28 +133,30 @@ ext {
|
||||
junitApi : "org.junit.jupiter:junit-jupiter-api:${jUnitVersion}",
|
||||
junitEngine : "org.junit.jupiter:junit-jupiter-engine:${jUnitVersion}",
|
||||
junitParams : "org.junit.jupiter:junit-jupiter-params:${jUnitVersion}",
|
||||
junit4 : "org.junit.jupiter:junit-jupiter:${jUnit4Version}",
|
||||
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}",
|
||||
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}",
|
||||
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}",
|
||||
rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}",
|
||||
rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}",
|
||||
rxBinding : "com.jakewharton.rxbinding2:rxbinding:${rxBindingVersion}",
|
||||
stax : "stax:stax:${staxVersion}",
|
||||
testingSupportLib : "com.android.support.test:testing-support-lib:${testingSupportLibVersion}",
|
||||
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}",
|
||||
rules : "androidx.test:rules:${rulesVersion}",
|
||||
contribution : "androidx.test.espresso:espresso-contrib:${contributionVersion}",
|
||||
uiAutomator : "androidx.test.uiautomator:uiautomator:${uiautomatorVersion}",
|
||||
zxcvbn : "com.nulab-inc:zxcvbn:${zxcvbnVersion}",
|
||||
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}",
|
||||
jsonWebTokenImpl : "io.jsonwebtoken:jjwt-impl:${jsonWebTokenApiVersion}",
|
||||
jsonWebTokenJson : "io.jsonwebtoken:jjwt-orgjson:${jsonWebTokenApiVersion}"
|
||||
|
@ -17,11 +17,15 @@ android {
|
||||
|
||||
buildConfigField 'int', 'VERSION_CODE', "${globalConfiguration["androidVersionCode"]}"
|
||||
buildConfigField "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\""
|
||||
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
@ -71,10 +75,14 @@ android {
|
||||
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/']
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/DEPENDENCIES'
|
||||
}
|
||||
}
|
||||
|
||||
greendao {
|
||||
schemaVersion 6
|
||||
schemaVersion 9
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
@ -90,6 +98,8 @@ dependencies {
|
||||
implementation project(':msa-auth-for-android')
|
||||
implementation project(':pcloud-sdk-java')
|
||||
|
||||
coreLibraryDesugaring dependencies.coreDesugaring
|
||||
|
||||
// cryptomator
|
||||
implementation dependencies.cryptolib
|
||||
|
||||
@ -98,32 +108,66 @@ dependencies {
|
||||
// dagger
|
||||
annotationProcessor dependencies.daggerCompiler
|
||||
implementation dependencies.dagger
|
||||
|
||||
api dependencies.jsonWebTokenApi
|
||||
implementation dependencies.jsonWebTokenImpl
|
||||
implementation dependencies.jsonWebTokenJson
|
||||
|
||||
// cloud
|
||||
implementation dependencies.awsAndroidS3
|
||||
implementation dependencies.dropbox
|
||||
implementation dependencies.msgraph
|
||||
|
||||
playstoreImplementation dependencies.googlePlayServicesAuth
|
||||
apkstoreImplementation dependencies.googlePlayServicesAuth
|
||||
implementation dependencies.stax
|
||||
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) {
|
||||
exclude module: 'guava-jdk5'
|
||||
exclude module: 'httpclient'
|
||||
exclude module: 'googlehttpclient'
|
||||
exclude group: "com.google.http-client", module: "google-http-client"
|
||||
}
|
||||
apkstoreImplementation(dependencies.googleApiServicesDrive) {
|
||||
exclude module: 'guava-jdk5'
|
||||
exclude module: 'httpclient'
|
||||
exclude module: "google-http-client"
|
||||
exclude group: "com.google.http-client", module: "google-http-client"
|
||||
}
|
||||
|
||||
playstoreImplementation(dependencies.googleApiClientAndroid) {
|
||||
exclude module: 'guava-jdk5'
|
||||
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) {
|
||||
exclude module: 'guava-jdk5'
|
||||
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
|
||||
implementation dependencies.rxJava
|
||||
implementation dependencies.rxAndroid
|
||||
@ -146,12 +190,16 @@ dependencies {
|
||||
testImplementation dependencies.junitApi
|
||||
testRuntimeOnly dependencies.junitEngine
|
||||
testImplementation dependencies.junitParams
|
||||
|
||||
testImplementation dependencies.junit4
|
||||
testRuntimeOnly dependencies.junit4Engine
|
||||
|
||||
testImplementation dependencies.mockito
|
||||
testImplementation dependencies.mockitoKotlin
|
||||
testImplementation dependencies.mockitoInline
|
||||
testImplementation dependencies.hamcrest
|
||||
|
||||
androidTestImplementation(dependencies.runner) {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
@ -161,3 +209,16 @@ configurations {
|
||||
static def getApiKey(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 com.google.common.base.Optional;
|
||||
|
||||
import org.cryptomator.cryptolib.api.Cryptor;
|
||||
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
|
||||
import org.cryptomator.domain.Cloud;
|
||||
import org.cryptomator.domain.Vault;
|
||||
import org.cryptomator.domain.exception.MissingCryptorException;
|
||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
@ -38,7 +39,7 @@ public class CryptoCloudContentRepositoryFactory implements CloudContentReposito
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
|
||||
public CloudContentRepository<CryptoCloud, CryptoNode, CryptoFolder, CryptoFile> cloudContentRepositoryFor(Cloud cloud) {
|
||||
CryptoCloud cryptoCloud = (CryptoCloud) cloud;
|
||||
Vault vault = cryptoCloud.getVault();
|
||||
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) {
|
||||
Optional<Cryptor> cryptor = cryptors.remove(vault);
|
||||
if (cryptor.isAbsent()) {
|
||||
if (!cryptor.isPresent()) {
|
||||
if (assertPresent) {
|
||||
throw new IllegalStateException(format("No cryptor registered for vault %s", vault));
|
||||
}
|
||||
|
@ -1,223 +1,97 @@
|
||||
package org.cryptomator.data.cloud.crypto;
|
||||
|
||||
import org.cryptomator.cryptolib.Cryptors;
|
||||
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 com.google.common.base.Optional;
|
||||
|
||||
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.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.vault.UnlockToken;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.text.Normalizer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import static android.R.attr.version;
|
||||
import static java.text.Normalizer.normalize;
|
||||
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.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME;
|
||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME;
|
||||
import static org.cryptomator.domain.Vault.aCopyOf;
|
||||
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
||||
|
||||
@Singleton
|
||||
public class CryptoCloudFactory {
|
||||
|
||||
private final CryptorProvider cryptorProvider;
|
||||
private final CloudContentRepository cloudContentRepository;
|
||||
private final CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile> cloudContentRepository;
|
||||
private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
@Inject
|
||||
public CryptoCloudFactory( //
|
||||
CloudContentRepository cloudContentRepository, //
|
||||
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory, //
|
||||
CryptorProvider cryptorProvider) {
|
||||
this.cryptorProvider = cryptorProvider;
|
||||
public CryptoCloudFactory(CloudContentRepository/*<Cloud, CloudNode, CloudFolder, CloudFile>*/ cloudContentRepository, //
|
||||
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) {
|
||||
this.cloudContentRepository = cloudContentRepository;
|
||||
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
|
||||
}
|
||||
|
||||
public void create(CloudFolder location, CharSequence password) throws BackendException {
|
||||
Cryptor cryptor = cryptorProvider.createNew();
|
||||
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);
|
||||
cryptoCloudProvider(Optional.absent()).create(location, password);
|
||||
}
|
||||
|
||||
public Cloud decryptedViewOf(Vault vault) throws BackendException {
|
||||
return new CryptoCloud(aCopyOf(vault).build());
|
||||
}
|
||||
|
||||
public Vault unlock(Vault vault, CharSequence password, Flag cancelledFlag) 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 {
|
||||
public Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException {
|
||||
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 {
|
||||
byte[] keyFileData = readKeyFileData(location);
|
||||
UnlockTokenImpl unlockToken = new UnlockTokenImpl(vault, keyFileData);
|
||||
assertVaultVersionIsSupported(unlockToken.getKeyFile().getVersion());
|
||||
return unlockToken;
|
||||
private byte[] readConfigFileData(CloudFolder location) throws BackendException {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME);
|
||||
cloudContentRepository.read(vaultFile, null, data, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD);
|
||||
return data.toByteArray();
|
||||
}
|
||||
|
||||
private Cryptor cryptorFor(KeyFile keyFile, CharSequence password) {
|
||||
return cryptorProvider.createFromKeyFile(keyFile, normalizePassword(password, keyFile.getVersion()), keyFile.getVersion());
|
||||
public Vault unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||
return cryptoCloudProvider(unverifiedVaultConfig).unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag);
|
||||
}
|
||||
|
||||
private CloudFolder vaultLocation(Vault vault) throws BackendException {
|
||||
return cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
||||
public Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||
return cryptoCloudProvider(unverifiedVaultConfig).unlock(token, unverifiedVaultConfig, password, cancelledFlag);
|
||||
}
|
||||
|
||||
public boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException {
|
||||
try {
|
||||
// create a cryptor, which checks the password, then destroy it immediately
|
||||
cryptorFor(createUnlockToken(vault).getKeyFile(), password).destroy();
|
||||
return true;
|
||||
} catch (InvalidPassphraseException e) {
|
||||
return false;
|
||||
}
|
||||
public UnlockToken createUnlockToken(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException {
|
||||
return cryptoCloudProvider(unverifiedVaultConfig).createUnlockToken(vault, unverifiedVaultConfig);
|
||||
}
|
||||
|
||||
public boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException {
|
||||
return cryptoCloudProvider(unverifiedVaultConfig).isVaultPasswordValid(vault, unverifiedVaultConfig, password);
|
||||
}
|
||||
|
||||
public void lock(Vault vault) {
|
||||
cryptoCloudContentRepositoryFactory.deregisterCryptor(vault);
|
||||
}
|
||||
|
||||
private void assertVaultVersionIsSupported(int version) {
|
||||
if (version < MIN_VAULT_VERSION) {
|
||||
throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION);
|
||||
} else if (version > MAX_VAULT_VERSION) {
|
||||
throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION);
|
||||
}
|
||||
public void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException {
|
||||
cryptoCloudProvider(unverifiedVaultConfig).changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword);
|
||||
}
|
||||
|
||||
private void writeKeyFile(CloudFolder location, KeyFile keyFile) throws BackendException {
|
||||
byte[] data = keyFile.serialize();
|
||||
cloudContentRepository.write(masterkeyFile(location), ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, false, data.length);
|
||||
}
|
||||
|
||||
private byte[] readKeyFileData(CloudFolder location) throws BackendException {
|
||||
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);
|
||||
private CryptoCloudProvider cryptoCloudProvider(Optional<UnverifiedVaultConfig> unverifiedVaultConfigOptional) {
|
||||
if (unverifiedVaultConfigOptional.isPresent()) {
|
||||
if (MASTERKEY_SCHEME.equals(unverifiedVaultConfigOptional.get().getKeyId().getScheme())) {
|
||||
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
|
||||
}
|
||||
throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme()));
|
||||
} 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
|
||||
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
|
||||
public CloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile> cloudContentRepositoryFor(Cloud cloud) {
|
||||
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