Merge branch 'develop' into release/1.6.0
This commit is contained in:
commit
c88ba3b710
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
|
||||
|
2
.idea/codeStyles/Project.xml
generated
2
.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,6 +179,7 @@
|
||||
</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" />
|
||||
|
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" />
|
||||
|
@ -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
|
||||
|
@ -1,18 +1,16 @@
|
||||
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.10'
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||
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 +40,7 @@ allprojects {
|
||||
ext {
|
||||
androidApplicationId = 'org.cryptomator'
|
||||
androidVersionCode = getVersionCode()
|
||||
androidVersionName = '1.6.0-alpha1'
|
||||
androidVersionName = '1.6.0-beta1'
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ allprojects {
|
||||
}
|
||||
|
||||
ext {
|
||||
androidBuildToolsVersion = "29.0.3"
|
||||
androidBuildToolsVersion = "30.0.2"
|
||||
androidMinSdkVersion = 24
|
||||
androidTargetSdkVersion = 29
|
||||
androidCompileSdkVersion = 29
|
||||
@ -17,8 +17,10 @@ ext {
|
||||
|
||||
// support lib
|
||||
androidSupportAnnotationsVersion = '1.2.0'
|
||||
androidSupportAppcompatVersion = '1.2.0'
|
||||
androidSupportDesignVersion = '1.3.0'
|
||||
androidSupportAppcompatVersion = '1.3.0'
|
||||
androidSupportDesignVersion = '1.4.0'
|
||||
|
||||
coreDesugaringVersion = '1.1.5'
|
||||
|
||||
// app frameworks and utilities
|
||||
|
||||
@ -26,9 +28,9 @@ ext {
|
||||
rxAndroidVersion = '2.1.1'
|
||||
rxBindingVersion = '2.2.0'
|
||||
|
||||
daggerVersion = '2.35.1'
|
||||
daggerVersion = '2.37'
|
||||
|
||||
gsonVersion = '2.8.6'
|
||||
gsonVersion = '2.8.7'
|
||||
|
||||
okHttpVersion = '4.9.1'
|
||||
okHttpDigestVersion = '2.5'
|
||||
@ -37,7 +39,7 @@ ext {
|
||||
|
||||
timberVersion = '4.7.1'
|
||||
|
||||
zxcvbnVersion = '1.5.0'
|
||||
zxcvbnVersion = '1.5.2'
|
||||
|
||||
scaleImageViewVersion = '3.10.0'
|
||||
|
||||
@ -47,20 +49,19 @@ ext {
|
||||
greenDaoVersion = '3.3.0'
|
||||
|
||||
// cloud provider libs
|
||||
|
||||
// do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x
|
||||
cryptolibVersion = '2.0.0-rc1'
|
||||
|
||||
awsAndroidSdkS3 = '2.23.0'
|
||||
cryptolibVersion = '2.0.0-rc6'
|
||||
|
||||
dropboxVersion = '4.0.0'
|
||||
|
||||
googleApiServicesVersion = 'v3-rev197-1.25.0'
|
||||
googlePlayServicesVersion = '19.0.0'
|
||||
googleClientVersion = '1.31.4'
|
||||
googleClientVersion = '1.32.1'
|
||||
|
||||
msgraphVersion = '2.10.0'
|
||||
|
||||
minIoVersion = '8.2.2'
|
||||
staxVersion = '1.2.0' // needed for minIO
|
||||
|
||||
commonsCodecVersion = '1.15'
|
||||
|
||||
recyclerViewFastScrollVersion = '2.0.1'
|
||||
@ -70,26 +71,26 @@ ext {
|
||||
jUnitVersion = '5.7.1'
|
||||
jUnit4Version = '4.13.1'
|
||||
assertJVersion = '1.7.1'
|
||||
mockitoVersion = '3.9.0'
|
||||
mockitoInlineVersion = '3.9.0'
|
||||
mockitoVersion = '3.11.2'
|
||||
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.3'
|
||||
androidxCoreVersion = '1.6.0'
|
||||
androidxFragmentVersion = '1.3.5'
|
||||
androidxViewpagerVersion = '1.0.0'
|
||||
androidxSwiperefreshVersion = '1.1.0'
|
||||
androidxPreferenceVersion = '1.1.1'
|
||||
androidxRecyclerViewVersion = '1.2.0'
|
||||
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 +104,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,6 +112,7 @@ 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}",
|
||||
@ -127,9 +128,11 @@ ext {
|
||||
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}",
|
||||
@ -137,6 +140,7 @@ ext {
|
||||
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}",
|
||||
|
@ -24,6 +24,8 @@ android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
@ -76,7 +78,7 @@ android {
|
||||
}
|
||||
|
||||
greendao {
|
||||
schemaVersion 7
|
||||
schemaVersion 8
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
@ -92,6 +94,8 @@ dependencies {
|
||||
implementation project(':msa-auth-for-android')
|
||||
implementation project(':pcloud-sdk-java')
|
||||
|
||||
coreLibraryDesugaring dependencies.coreDesugaring
|
||||
|
||||
// cryptomator
|
||||
implementation dependencies.cryptolib
|
||||
|
||||
@ -106,10 +110,12 @@ dependencies {
|
||||
implementation dependencies.jsonWebTokenJson
|
||||
|
||||
// cloud
|
||||
implementation dependencies.awsAndroidS3
|
||||
implementation dependencies.dropbox
|
||||
implementation dependencies.msgraph
|
||||
|
||||
implementation dependencies.stax
|
||||
api dependencies.minIo
|
||||
|
||||
playstoreImplementation dependencies.googlePlayServicesAuth
|
||||
apkstoreImplementation dependencies.googlePlayServicesAuth
|
||||
|
||||
@ -158,6 +164,7 @@ dependencies {
|
||||
testRuntimeOnly dependencies.junit4Engine
|
||||
|
||||
testImplementation dependencies.mockito
|
||||
testImplementation dependencies.mockitoKotlin
|
||||
testImplementation dependencies.mockitoInline
|
||||
testImplementation dependencies.hamcrest
|
||||
}
|
||||
|
@ -1,178 +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 | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DirType resolve(CloudType cloud, String path) throws BackendException {
|
||||
try {
|
||||
return delegate.resolve(cloud, path);
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileType file(DirType parent, String name) throws BackendException {
|
||||
try {
|
||||
return delegate.file(parent, name);
|
||||
} catch (BackendException | 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 | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DirType folder(DirType parent, String name) throws BackendException {
|
||||
try {
|
||||
return delegate.folder(parent, name);
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(NodeType node) throws BackendException {
|
||||
try {
|
||||
return delegate.exists(node);
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends CloudNode> list(DirType folder) throws BackendException {
|
||||
try {
|
||||
return delegate.list(folder);
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DirType create(DirType folder) throws BackendException {
|
||||
try {
|
||||
return delegate.create(folder);
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DirType move(DirType source, DirType target) throws BackendException {
|
||||
try {
|
||||
return delegate.move(source, target);
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileType move(FileType source, FileType target) throws BackendException {
|
||||
try {
|
||||
return delegate.move(source, target);
|
||||
} catch (BackendException | 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 | 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 | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(NodeType node) throws BackendException {
|
||||
try {
|
||||
delegate.delete(node);
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String checkAuthenticationAndRetrieveCurrentAccount(CloudType cloud) throws BackendException {
|
||||
try {
|
||||
return delegate.checkAuthenticationAndRetrieveCurrentAccount(cloud);
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(CloudType cloud) throws BackendException {
|
||||
try {
|
||||
delegate.logout(cloud);
|
||||
} catch (BackendException | 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,138 +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().getFormat()) {
|
||||
case 7:
|
||||
this.cryptoImpl = new CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7());
|
||||
break;
|
||||
case 8:
|
||||
this.cryptoImpl = new CryptoImplVaultFormat8(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7(), cloud.getVault().getShorteningThreshold());
|
||||
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 format %d.", cloud.getVault().getFormat()));
|
||||
}
|
||||
}
|
||||
|
||||
@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,17 +1,21 @@
|
||||
package org.cryptomator.data.cloud.crypto;
|
||||
|
||||
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.repository.CloudContentRepository;
|
||||
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.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@ -20,25 +24,23 @@ import javax.inject.Singleton;
|
||||
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;
|
||||
import static org.cryptomator.util.Encodings.UTF_8;
|
||||
|
||||
@Singleton
|
||||
public class CryptoCloudFactory {
|
||||
|
||||
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, //
|
||||
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 {
|
||||
cryptoCloudProvider(Optional.empty()).create(location, password);
|
||||
cryptoCloudProvider(Optional.absent()).create(location, password);
|
||||
}
|
||||
|
||||
public Cloud decryptedViewOf(Vault vault) throws BackendException {
|
||||
@ -47,14 +49,14 @@ public class CryptoCloudFactory {
|
||||
|
||||
public Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException {
|
||||
CloudFolder vaultLocation = cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
||||
String jwt = new String(readConfigFileData(vaultLocation), UTF_8);
|
||||
String jwt = new String(readConfigFileData(vaultLocation), StandardCharsets.UTF_8);
|
||||
return Optional.of(VaultConfig.decode(jwt));
|
||||
}
|
||||
|
||||
private byte[] readConfigFileData(CloudFolder location) throws BackendException {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME);
|
||||
cloudContentRepository.read(vaultFile, Optional.empty(), data, NO_OP_PROGRESS_AWARE);
|
||||
cloudContentRepository.read(vaultFile, null, data, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD);
|
||||
return data.toByteArray();
|
||||
}
|
||||
|
||||
@ -84,12 +86,10 @@ public class CryptoCloudFactory {
|
||||
|
||||
private CryptoCloudProvider cryptoCloudProvider(Optional<UnverifiedVaultConfig> unverifiedVaultConfigOptional) {
|
||||
if (unverifiedVaultConfigOptional.isPresent()) {
|
||||
switch (unverifiedVaultConfigOptional.get().getKeyId().getScheme()) {
|
||||
case MASTERKEY_SCHEME: {
|
||||
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
|
||||
}
|
||||
default: throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme()));
|
||||
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 new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
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;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
public interface CryptoCloudProvider {
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
package org.cryptomator.data.cloud.crypto;
|
||||
|
||||
public class CryptoConstants {
|
||||
|
||||
public static final String MASTERKEY_SCHEME = "masterkeyfile";
|
||||
|
||||
static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator";
|
||||
|
||||
static final String ROOT_DIR_ID = "";
|
||||
static final String DATA_DIR_NAME = "d";
|
||||
static final String VAULT_FILE_NAME = "vault.cryptomator";
|
||||
static final String MASTERKEY_BACKUP_FILE_EXT = ".bkup";
|
||||
|
||||
static final int DEFAULT_MASTERKEY_FILE_VERSION = 999;
|
||||
static final int MAX_VAULT_VERSION = 8;
|
||||
static final int MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG = 7;
|
||||
static final int VERSION_WITH_NORMALIZED_PASSWORDS = 6;
|
||||
static final int MIN_VAULT_VERSION = 5;
|
||||
|
||||
static final int DEFAULT_MAX_FILE_NAME = 220;
|
||||
|
||||
static final byte[] PEPPER = new byte[0];
|
||||
|
||||
static final VaultCipherCombo DEFAULT_CIPHER_COMBO = VaultCipherCombo.SIV_CTRMAC;
|
||||
|
||||
}
|
@ -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,425 +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;
|
||||
final int shorteningThreshold;
|
||||
|
||||
private final Supplier<Cryptor> cryptor;
|
||||
private final CloudFolder storageLocation;
|
||||
|
||||
private RootCryptoFolder root;
|
||||
|
||||
CryptoImplDecorator(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int shorteningThreshold) {
|
||||
this.context = context;
|
||||
this.cryptor = cryptor;
|
||||
this.cloudContentRepository = cloudContentRepository;
|
||||
this.storageLocation = storageLocation;
|
||||
this.dirIdCache = dirIdCache;
|
||||
this.shorteningThreshold = shorteningThreshold;
|
||||
}
|
||||
|
||||
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,549 +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;
|
||||
|
||||
class CryptoImplVaultFormat7 extends CryptoImplDecorator {
|
||||
|
||||
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, CryptoConstants.DEFAULT_MAX_FILE_NAME);
|
||||
}
|
||||
|
||||
CryptoImplVaultFormat7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int shorteningThreshold) {
|
||||
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold);
|
||||
}
|
||||
|
||||
@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() > shorteningThreshold) {
|
||||
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.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}==)?$")
|
||||
}
|
||||
}
|
@ -1,16 +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.repository.CloudContentRepository;
|
||||
import org.cryptomator.util.Supplier;
|
||||
|
||||
public class CryptoImplVaultFormat8 extends CryptoImplVaultFormat7 {
|
||||
|
||||
CryptoImplVaultFormat8(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int shorteningThreshold) {
|
||||
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold);
|
||||
}
|
||||
|
||||
}
|
@ -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,269 +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 {
|
||||
|
||||
static final int SHORTENING_THRESHOLD = 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, SHORTENING_THRESHOLD);
|
||||
}
|
||||
|
||||
@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() > shorteningThreshold) {
|
||||
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() <= shorteningThreshold) {
|
||||
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
|
||||
}
|
||||
}
|
@ -1,324 +0,0 @@
|
||||
package org.cryptomator.data.cloud.crypto;
|
||||
|
||||
import org.cryptomator.cryptolib.api.Cryptor;
|
||||
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.domain.CloudFile;
|
||||
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.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.cloud.ByteArrayDataSource;
|
||||
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.Normalizer;
|
||||
|
||||
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.DEFAULT_CIPHER_COMBO;
|
||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_MASTERKEY_FILE_VERSION;
|
||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_MAX_FILE_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.MASTERKEY_SCHEME;
|
||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION;
|
||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG;
|
||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MIN_VAULT_VERSION;
|
||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.PEPPER;
|
||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.ROOT_DIR_ID;
|
||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME;
|
||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VERSION_WITH_NORMALIZED_PASSWORDS;
|
||||
import static org.cryptomator.data.cloud.crypto.VaultCipherCombo.SIV_CTRMAC;
|
||||
import static org.cryptomator.domain.Vault.aCopyOf;
|
||||
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
||||
import static org.cryptomator.util.Encodings.UTF_8;
|
||||
|
||||
public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider {
|
||||
|
||||
private final CloudContentRepository cloudContentRepository;
|
||||
private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
|
||||
private final SecureRandom secureRandom;
|
||||
|
||||
public MasterkeyCryptoCloudProvider(CloudContentRepository cloudContentRepository, //
|
||||
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory, //
|
||||
SecureRandom secureRandom) {
|
||||
this.cloudContentRepository = cloudContentRepository;
|
||||
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
|
||||
this.secureRandom = secureRandom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(CloudFolder location, CharSequence password) throws BackendException {
|
||||
// Just for testing (id in VaultConfig is auto generated which makes sense while creating a vault but not for testing)
|
||||
create(location, password, VaultConfig.createVaultConfig());
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
void create(CloudFolder location, CharSequence password, VaultConfig.VaultConfigBuilder vaultConfigBuilder) throws BackendException {
|
||||
// 1. write masterkey:
|
||||
Masterkey masterkey = Masterkey.generate(secureRandom);
|
||||
try (ByteArrayOutputStream data = new ByteArrayOutputStream()) {
|
||||
new MasterkeyFileAccess(PEPPER, secureRandom).persist(masterkey, data, password, DEFAULT_MASTERKEY_FILE_VERSION);
|
||||
cloudContentRepository.write(legacyMasterkeyFile(location), ByteArrayDataSource.from(data.toByteArray()), NO_OP_PROGRESS_AWARE, false, data.size());
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException("Failed to write masterkey", e);
|
||||
}
|
||||
|
||||
// 2. initialize vault:
|
||||
VaultConfig vaultConfig = vaultConfigBuilder //
|
||||
.vaultFormat(MAX_VAULT_VERSION) //
|
||||
.cipherCombo(DEFAULT_CIPHER_COMBO) //
|
||||
.keyId(URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME))) //
|
||||
.shorteningThreshold(DEFAULT_MAX_FILE_NAME) //
|
||||
.build();
|
||||
|
||||
byte[] encodedVaultConfig = vaultConfig.toToken(masterkey.getEncoded()).getBytes(UTF_8);
|
||||
CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME);
|
||||
cloudContentRepository.write(vaultFile, ByteArrayDataSource.from(encodedVaultConfig), NO_OP_PROGRESS_AWARE, false, encodedVaultConfig.length);
|
||||
|
||||
// 3. create root folder:
|
||||
createRootFolder(location, cryptorFor(masterkey, vaultConfig.getCipherCombo()));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vault unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||
return unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||
UnlockTokenImpl impl = (UnlockTokenImpl) token;
|
||||
try {
|
||||
Masterkey masterkey = impl.getKeyFile(password);
|
||||
|
||||
int vaultFormat;
|
||||
int shorteningThreshold;
|
||||
Cryptor cryptor;
|
||||
|
||||
if (unverifiedVaultConfig.isPresent()) {
|
||||
VaultConfig vaultConfig = VaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.get());
|
||||
vaultFormat = vaultConfig.getVaultFormat();
|
||||
assertVaultVersionIsSupported(vaultConfig.getVaultFormat());
|
||||
shorteningThreshold = vaultConfig.getShorteningThreshold();
|
||||
cryptor = cryptorFor(masterkey, vaultConfig.getCipherCombo());
|
||||
} else {
|
||||
vaultFormat = MasterkeyFileAccess.readAllegedVaultVersion(impl.keyFileData);
|
||||
assertLegacyVaultVersionIsSupported(vaultFormat);
|
||||
shorteningThreshold = vaultFormat > 6 ? CryptoConstants.DEFAULT_MAX_FILE_NAME : CryptoImplVaultFormatPre7.SHORTENING_THRESHOLD;
|
||||
cryptor = cryptorFor(masterkey, SIV_CTRMAC);
|
||||
}
|
||||
|
||||
|
||||
if (cancelledFlag.get()) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
|
||||
Vault vault = aCopyOf(token.getVault()) //
|
||||
.withUnlocked(true) //
|
||||
.withFormat(vaultFormat) //
|
||||
.withShorteningThreshold(shorteningThreshold) //
|
||||
.build();
|
||||
|
||||
cryptoCloudContentRepositoryFactory.registerCryptor(vault, cryptor);
|
||||
|
||||
return vault;
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnlockTokenImpl createUnlockToken(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException {
|
||||
CloudFolder vaultLocation = vaultLocation(vault);
|
||||
if (unverifiedVaultConfig.isPresent()) {
|
||||
return createUnlockToken(vault, masterkeyFile(vaultLocation, unverifiedVaultConfig.get()));
|
||||
} else {
|
||||
return createUnlockToken(vault, legacyMasterkeyFile(vaultLocation));
|
||||
}
|
||||
}
|
||||
|
||||
private CloudFile masterkeyFile(CloudFolder vaultLocation, UnverifiedVaultConfig unverifiedVaultConfig) throws BackendException {
|
||||
String path = unverifiedVaultConfig.getKeyId().getSchemeSpecificPart();
|
||||
if(!path.equals(MASTERKEY_FILE_NAME)) {
|
||||
throw new UnsupportedMasterkeyLocationException(unverifiedVaultConfig);
|
||||
}
|
||||
return cloudContentRepository.file(vaultLocation, path);
|
||||
}
|
||||
|
||||
private CloudFile legacyMasterkeyFile(CloudFolder location) throws BackendException {
|
||||
return cloudContentRepository.file(location, MASTERKEY_FILE_NAME);
|
||||
}
|
||||
|
||||
private UnlockTokenImpl createUnlockToken(Vault vault, CloudFile location) throws BackendException {
|
||||
byte[] keyFileData = readKeyFileData(location);
|
||||
UnlockTokenImpl unlockToken = new UnlockTokenImpl(vault, keyFileData);
|
||||
return unlockToken;
|
||||
}
|
||||
|
||||
private byte[] readKeyFileData(CloudFile masterkeyFile) throws BackendException {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
cloudContentRepository.read(masterkeyFile, Optional.empty(), data, NO_OP_PROGRESS_AWARE);
|
||||
return data.toByteArray();
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
Cryptor cryptorFor(Masterkey keyFile, VaultCipherCombo vaultCipherCombo) {
|
||||
return vaultCipherCombo.getCryptorProvider(secureRandom).withKey(keyFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException {
|
||||
try {
|
||||
// create a cryptor, which checks the password, then destroy it immediately
|
||||
UnlockTokenImpl unlockToken = createUnlockToken(vault, unverifiedVaultConfig);
|
||||
Masterkey masterkey = unlockToken.getKeyFile(password);
|
||||
VaultCipherCombo vaultCipherCombo;
|
||||
if (unverifiedVaultConfig.isPresent()) {
|
||||
VaultConfig vaultConfig = VaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.get());
|
||||
assertVaultVersionIsSupported(vaultConfig.getVaultFormat());
|
||||
vaultCipherCombo = vaultConfig.getCipherCombo();
|
||||
} else {
|
||||
int vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(unlockToken.keyFileData);
|
||||
assertLegacyVaultVersionIsSupported(vaultVersion);
|
||||
vaultCipherCombo = SIV_CTRMAC;
|
||||
}
|
||||
cryptorFor(masterkey, vaultCipherCombo).destroy();
|
||||
return true;
|
||||
} catch (InvalidPassphraseException e) {
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertLegacyVaultVersionIsSupported(int version) {
|
||||
if (version < MIN_VAULT_VERSION) {
|
||||
throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION);
|
||||
} else if (version > MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG) {
|
||||
throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException {
|
||||
CloudFolder vaultLocation = vaultLocation(vault);
|
||||
CloudFile masterkeyFile;
|
||||
if (unverifiedVaultConfig.isPresent()) {
|
||||
masterkeyFile = masterkeyFile(vaultLocation, unverifiedVaultConfig.get());
|
||||
} else {
|
||||
masterkeyFile = legacyMasterkeyFile(vaultLocation);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream();
|
||||
cloudContentRepository.read(masterkeyFile, Optional.empty(), dataOutputStream, NO_OP_PROGRESS_AWARE);
|
||||
byte[] data = dataOutputStream.toByteArray();
|
||||
|
||||
int vaultVersion;
|
||||
if (unverifiedVaultConfig.isPresent()) {
|
||||
vaultVersion = unverifiedVaultConfig.get().getVaultFormat();
|
||||
assertVaultVersionIsSupported(vaultVersion);
|
||||
} else {
|
||||
try {
|
||||
vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(data);
|
||||
assertLegacyVaultVersionIsSupported(vaultVersion);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException("Failed to read legacy vault version", e);
|
||||
}
|
||||
}
|
||||
|
||||
createBackupMasterKeyFile(data, masterkeyFile);
|
||||
createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, masterkeyFile);
|
||||
}
|
||||
|
||||
private CloudFolder vaultLocation(Vault vault) throws BackendException {
|
||||
return cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
||||
}
|
||||
|
||||
private void createBackupMasterKeyFile(byte[] data, CloudFile masterkeyFile) throws BackendException {
|
||||
cloudContentRepository.write(masterkeyBackupFile(masterkeyFile, data), ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, true, data.length);
|
||||
}
|
||||
|
||||
private CloudFile masterkeyBackupFile(CloudFile masterkeyFile, byte[] data) throws BackendException {
|
||||
String fileName = masterkeyFile.getName() + BackupFileIdSuffixGenerator.generate(data) + MASTERKEY_BACKUP_FILE_EXT;
|
||||
return cloudContentRepository.file(masterkeyFile.getParent(), fileName);
|
||||
}
|
||||
|
||||
private void createNewMasterKeyFile(byte[] data, int vaultVersion, String oldPassword, String newPassword, CloudFile masterkeyFile) throws BackendException {
|
||||
try {
|
||||
byte[] newMasterKeyFile = new MasterkeyFileAccess(PEPPER, secureRandom) //
|
||||
.changePassphrase(data, normalizePassword(oldPassword, vaultVersion), normalizePassword(newPassword, vaultVersion));
|
||||
cloudContentRepository.write(masterkeyFile, //
|
||||
ByteArrayDataSource.from(newMasterKeyFile), //
|
||||
NO_OP_PROGRESS_AWARE, //
|
||||
true, //
|
||||
newMasterKeyFile.length);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException("Failed to read legacy vault version", e);
|
||||
}
|
||||
}
|
||||
|
||||
private CharSequence normalizePassword(CharSequence password, int vaultVersion) {
|
||||
if (vaultVersion >= VERSION_WITH_NORMALIZED_PASSWORDS) {
|
||||
return normalize(password, Normalizer.Form.NFC);
|
||||
} else {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
|
||||
static class UnlockTokenImpl implements UnlockToken {
|
||||
|
||||
private final Vault vault;
|
||||
private final byte[] keyFileData;
|
||||
|
||||
UnlockTokenImpl(Vault vault, byte[] keyFileData) {
|
||||
this.vault = vault;
|
||||
this.keyFileData = keyFileData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vault getVault() {
|
||||
return vault;
|
||||
}
|
||||
|
||||
public Masterkey getKeyFile(CharSequence password) throws IOException {
|
||||
return new MasterkeyFileAccess(PEPPER, new SecureRandom()).load(new ByteArrayInputStream(keyFileData), password);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package org.cryptomator.data.cloud.crypto;
|
||||
|
||||
import org.cryptomator.cryptolib.Cryptors;
|
||||
import org.cryptomator.cryptolib.api.CryptorProvider;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* A combination of different ciphers and/or cipher modes in a Cryptomator vault.
|
||||
*/
|
||||
public enum VaultCipherCombo {
|
||||
/**
|
||||
* AES-SIV for file name encryption
|
||||
* AES-CTR + HMAC for content encryption
|
||||
*/
|
||||
SIV_CTRMAC(Cryptors::version1),
|
||||
|
||||
/**
|
||||
* AES-SIV for file name encryption
|
||||
* AES-GCM for content encryption
|
||||
*/
|
||||
SIV_GCM(Cryptors::version2);
|
||||
|
||||
private final Function<SecureRandom, CryptorProvider> cryptorProvider;
|
||||
|
||||
VaultCipherCombo(Function<SecureRandom, CryptorProvider> cryptorProvider) {
|
||||
this.cryptorProvider = cryptorProvider;
|
||||
}
|
||||
|
||||
public CryptorProvider getCryptorProvider(SecureRandom csprng) {
|
||||
return cryptorProvider.apply(csprng);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
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
|
||||
@ -23,25 +24,25 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) {
|
||||
val keyId: URI
|
||||
val id: String
|
||||
val vaultFormat: Int
|
||||
val cipherCombo: VaultCipherCombo
|
||||
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()
|
||||
.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 = VaultCipherCombo.SIV_CTRMAC
|
||||
internal var cipherCombo = CryptoConstants.DEFAULT_CIPHER_COMBO
|
||||
internal var shorteningThreshold = CryptoConstants.DEFAULT_MAX_FILE_NAME;
|
||||
lateinit var keyId: URI
|
||||
|
||||
@ -50,7 +51,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) {
|
||||
return this
|
||||
}
|
||||
|
||||
fun cipherCombo(cipherCombo: VaultCipherCombo): VaultConfigBuilder {
|
||||
fun cipherCombo(cipherCombo: CryptorProvider.Scheme): VaultConfigBuilder {
|
||||
this.cipherCombo = cipherCombo
|
||||
return this
|
||||
}
|
||||
@ -101,25 +102,24 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) {
|
||||
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)
|
||||
.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(VaultCipherCombo.valueOf(parser.body.get(JSON_KEY_CIPHERCONFIG, String::class.java))) //
|
||||
.vaultFormat(unverifiedVaultConfig.vaultFormat) //
|
||||
.shorteningThreshold(parser.body[JSON_KEY_SHORTENING_THRESHOLD] as Int)
|
||||
.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: Exception) {
|
||||
} catch (e: JwtException) {
|
||||
when (e) {
|
||||
is MissingClaimException, is IncorrectClaimException -> throw VaultVersionMismatchException("Vault config not for version " + unverifiedVaultConfig.vaultFormat)
|
||||
is SignatureException -> throw VaultKeyInvalidException()
|
||||
is JwtException -> throw VaultConfigLoadException("Failed to verify vault config", e)
|
||||
else -> throw VaultConfigLoadException(e)
|
||||
}
|
||||
}
|
||||
|
@ -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 (!replace && exists(file)) {
|
||||
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 (!replace && exists(file)) {
|
||||
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 (!replace && fileUri.isPresent()) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||
|
||||
import android.database.Cursor
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.util.Date
|
||||
|
||||
internal object LocalStorageAccessFrameworkNodeFactory {
|
||||
|
||||
fun from(parent: LocalStorageAccessFolder, cursor: Cursor): LocalStorageAccessNode {
|
||||
return if (isFolder(cursor)) {
|
||||
folder(parent, cursor)
|
||||
} else {
|
||||
file(parent, cursor)
|
||||
}
|
||||
}
|
||||
|
||||
private fun file(parent: LocalStorageAccessFolder, cursor: Cursor): LocalStorageAccessFile {
|
||||
return LocalStorageAccessFile( //
|
||||
parent, //
|
||||
cursor.getString(0), //
|
||||
getNodePath(parent, cursor.getString(0)), //
|
||||
cursor.getLong(2), //
|
||||
Date(cursor.getLong(3)), //
|
||||
cursor.getString(4), //
|
||||
getDocumentUri(parent, cursor.getString(4))
|
||||
)
|
||||
}
|
||||
|
||||
private fun folder(parent: LocalStorageAccessFolder, cursor: Cursor): LocalStorageAccessFolder {
|
||||
return LocalStorageAccessFolder(
|
||||
parent, //
|
||||
cursor.getString(0), //
|
||||
getNodePath(parent, cursor.getString(0)), //
|
||||
cursor.getString(4), //
|
||||
getDocumentUri(parent, cursor.getString(4))
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun from(parent: LocalStorageAccessFolder, documentFile: DocumentFile): LocalStorageAccessNode {
|
||||
return if (isFolder(documentFile)) {
|
||||
folder(parent, documentFile)
|
||||
} else {
|
||||
file(parent, documentFile)
|
||||
}
|
||||
}
|
||||
|
||||
fun folder(parent: LocalStorageAccessFolder, directory: DocumentFile): LocalStorageAccessFolder {
|
||||
return LocalStorageAccessFolder(
|
||||
parent, //
|
||||
directory.name!!, // FIXME
|
||||
getNodePath(parent, directory.name), //
|
||||
DocumentsContract.getDocumentId(directory.uri), //
|
||||
directory.uri.toString()
|
||||
)
|
||||
}
|
||||
|
||||
fun file(parent: LocalStorageAccessFolder, documentFile: DocumentFile): LocalStorageAccessFile {
|
||||
return LocalStorageAccessFile( //
|
||||
parent, //
|
||||
documentFile.name!!, // FIXME
|
||||
getNodePath(parent, documentFile.name), //
|
||||
documentFile.length(), //
|
||||
Date(documentFile.lastModified()), //
|
||||
DocumentsContract.getTreeDocumentId(documentFile.uri), //
|
||||
documentFile.uri.toString()
|
||||
)
|
||||
}
|
||||
|
||||
fun file(parent: LocalStorageAccessFolder, name: String, size: Long?): LocalStorageAccessFile {
|
||||
return LocalStorageAccessFile( //
|
||||
parent, //
|
||||
name, //
|
||||
getNodePath(parent, name), //
|
||||
size, //
|
||||
null, //
|
||||
null, //
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun file(parent: LocalStorageAccessFolder, name: String, path: String, size: Long?, documentId: String): LocalStorageAccessFile {
|
||||
return LocalStorageAccessFile(
|
||||
parent, //
|
||||
name, //
|
||||
path, //
|
||||
size, //
|
||||
null, //
|
||||
documentId, //
|
||||
getDocumentUri(parent, documentId)
|
||||
)
|
||||
}
|
||||
|
||||
fun folder(parent: LocalStorageAccessFolder, name: String): LocalStorageAccessFolder {
|
||||
return LocalStorageAccessFolder(
|
||||
parent, //
|
||||
name, //
|
||||
getNodePath(parent, name), //
|
||||
null, //
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun folder(parent: LocalStorageAccessFolder, name: String, documentId: String): LocalStorageAccessFolder {
|
||||
return LocalStorageAccessFolder(
|
||||
parent, //
|
||||
name, //
|
||||
getNodePath(parent, name), //
|
||||
documentId, //
|
||||
getDocumentUri(parent, documentId)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDocumentUri(parent: LocalStorageAccessFolder, documentId: String): String {
|
||||
return DocumentsContract.buildDocumentUriUsingTree(parent.uri, documentId).toString()
|
||||
}
|
||||
|
||||
private fun isFolder(file: DocumentFile): Boolean {
|
||||
return file.isDirectory
|
||||
}
|
||||
|
||||
private fun isFolder(cursor: Cursor): Boolean {
|
||||
return cursor.getString(1) == DocumentsContract.Document.MIME_TYPE_DIR
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getNodePath(parent: LocalStorageAccessFolder, name: String?): String {
|
||||
return parent.path + "/" + name
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.cryptomator.domain.CloudNode;
|
||||
|
||||
public interface LocalStorageAccessNode extends CloudNode {
|
||||
|
||||
Uri getUri();
|
||||
|
||||
LocalStorageAccessFolder getParent();
|
||||
|
||||
String getDocumentId();
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||
|
||||
import android.net.Uri
|
||||
import org.cryptomator.domain.CloudNode
|
||||
|
||||
interface LocalStorageAccessNode : CloudNode {
|
||||
|
||||
val uri: Uri?
|
||||
override val parent: LocalStorageAccessFolder?
|
||||
val documentId: String?
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework;
|
||||
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.cryptomator.domain.Cloud;
|
||||
import org.cryptomator.domain.LocalStorageCloud;
|
||||
|
||||
import static android.net.Uri.parse;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public class RootLocalStorageAccessFolder extends LocalStorageAccessFolder {
|
||||
|
||||
private final LocalStorageCloud localStorageCloud;
|
||||
|
||||
public RootLocalStorageAccessFolder(LocalStorageCloud localStorageCloud) {
|
||||
super(null, //
|
||||
"", //
|
||||
"", //
|
||||
DocumentsContract.getTreeDocumentId( //
|
||||
parse(localStorageCloud.rootUri())), //
|
||||
DocumentsContract.buildChildDocumentsUriUsingTree( //
|
||||
parse(localStorageCloud.rootUri()), //
|
||||
DocumentsContract.getTreeDocumentId(parse(localStorageCloud.rootUri()))).toString());
|
||||
this.localStorageCloud = localStorageCloud;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cloud getCloud() {
|
||||
return localStorageCloud;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalStorageAccessFolder withCloud(Cloud cloud) {
|
||||
return new RootLocalStorageAccessFolder((LocalStorageCloud) cloud);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import org.cryptomator.domain.Cloud
|
||||
import org.cryptomator.domain.LocalStorageCloud
|
||||
|
||||
class RootLocalStorageAccessFolder(private val localStorageCloud: LocalStorageCloud) : LocalStorageAccessFolder(
|
||||
null, //
|
||||
"", //
|
||||
"", //
|
||||
DocumentsContract.getTreeDocumentId(Uri.parse(localStorageCloud.rootUri())), //
|
||||
DocumentsContract.buildChildDocumentsUriUsingTree( //
|
||||
Uri.parse(localStorageCloud.rootUri()), //
|
||||
DocumentsContract.getTreeDocumentId(Uri.parse(localStorageCloud.rootUri()))
|
||||
).toString()
|
||||
) {
|
||||
|
||||
override val cloud: Cloud
|
||||
get() = localStorageCloud
|
||||
|
||||
override fun withCloud(cloud: Cloud?): RootLocalStorageAccessFolder {
|
||||
return RootLocalStorageAccessFolder(cloud as LocalStorageCloud)
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package org.cryptomator.data.cloud.okhttplogging;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
class HeaderNames {
|
||||
|
||||
private final Set<String> lowercaseNames = new HashSet<>();
|
||||
|
||||
public HeaderNames(String... headerNames) {
|
||||
for (String headerName : headerNames) {
|
||||
lowercaseNames.add(headerName.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contains(String headerName) {
|
||||
return lowercaseNames.contains(headerName.toLowerCase());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.cryptomator.data.cloud.okhttplogging
|
||||
|
||||
import java.util.HashSet
|
||||
|
||||
internal class HeaderNames(vararg headerNames: String) {
|
||||
|
||||
private val lowercaseNames: MutableSet<String> = HashSet()
|
||||
|
||||
operator fun contains(headerName: String): Boolean {
|
||||
return lowercaseNames.contains(headerName.lowercase())
|
||||
}
|
||||
|
||||
init {
|
||||
headerNames.mapTo(lowercaseNames) { it.lowercase() }
|
||||
}
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
package org.cryptomator.data.cloud.okhttplogging;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.Connection;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.Protocol;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import static android.preference.PreferenceManager.getDefaultSharedPreferences;
|
||||
import static java.lang.String.format;
|
||||
import static java.util.concurrent.TimeUnit.NANOSECONDS;
|
||||
|
||||
public final class HttpLoggingInterceptor implements Interceptor {
|
||||
|
||||
private static final HeaderNames EXCLUDED_HEADERS = new HeaderNames(//
|
||||
// headers excluded because they are logged separately:
|
||||
"Content-Type", "Content-Length",
|
||||
// headers excluded because they contain sensitive information:
|
||||
"Authorization", //
|
||||
"WWW-Authenticate", //
|
||||
"Cookie", //
|
||||
"Set-Cookie" //
|
||||
);
|
||||
private final Logger logger;
|
||||
private final Context context;
|
||||
|
||||
public HttpLoggingInterceptor(Logger logger, Context context) {
|
||||
this.logger = logger;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private static boolean debugModeEnabled(Context context) {
|
||||
return getDefaultSharedPreferences(context).getBoolean("debugMode", false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Response intercept(@NotNull Chain chain) throws IOException {
|
||||
if (debugModeEnabled(context)) {
|
||||
return proceedWithLogging(chain);
|
||||
} else {
|
||||
return chain.proceed(chain.request());
|
||||
}
|
||||
}
|
||||
|
||||
private Response proceedWithLogging(Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
logRequest(request, chain);
|
||||
return getAndLogResponse(request, chain);
|
||||
}
|
||||
|
||||
private void logRequest(Request request, Chain chain) throws IOException {
|
||||
logRequestStart(request, chain);
|
||||
logContentTypeAndLength(request);
|
||||
logHeaders(request.headers());
|
||||
logRequestEnd(request);
|
||||
}
|
||||
|
||||
private Response getAndLogResponse(Request request, Chain chain) throws IOException {
|
||||
long startOfRequestMs = System.nanoTime();
|
||||
Response response = getResponseLoggingExceptions(request, chain);
|
||||
long requestDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startOfRequestMs);
|
||||
logResponse(response, requestDurationMs);
|
||||
return response;
|
||||
}
|
||||
|
||||
private Response getResponseLoggingExceptions(Request request, Chain chain) throws IOException {
|
||||
try {
|
||||
return chain.proceed(request);
|
||||
} catch (Exception e) {
|
||||
logger.log("<-- HTTP FAILED: " + e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void logResponse(Response response, long requestDurationMs) {
|
||||
logResponseStart(response, requestDurationMs);
|
||||
logHeaders(response.headers());
|
||||
logger.log("<-- END HTTP");
|
||||
}
|
||||
|
||||
private void logRequestStart(Request request, Chain chain) throws IOException {
|
||||
Connection connection = chain.connection();
|
||||
Protocol protocol = connection != null ? connection.protocol() : Protocol.HTTP_1_1;
|
||||
String bodyLength = hasBody(request) ? request.body().contentLength() + "-byte body" : "unknown length";
|
||||
|
||||
logger.log(format("--> %s %s %s (%s)", //
|
||||
request.method(), //
|
||||
request.url(), //
|
||||
protocol, //
|
||||
bodyLength //
|
||||
));
|
||||
}
|
||||
|
||||
private void logContentTypeAndLength(Request request) throws IOException {
|
||||
// Request body headers are only present when installed as a network interceptor. Force
|
||||
// them to be included (when available) so there values are known.
|
||||
if (hasBody(request)) {
|
||||
RequestBody body = request.body();
|
||||
if (body.contentType() != null) {
|
||||
logger.log("Content-Type: " + body.contentType());
|
||||
}
|
||||
if (body.contentLength() != -1) {
|
||||
logger.log("Content-Length: " + body.contentLength());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void logRequestEnd(Request request) {
|
||||
logger.log("--> END " + request.method());
|
||||
}
|
||||
|
||||
private void logResponseStart(Response response, long requestDurationMs) {
|
||||
logger.log("<-- " + response.code() + ' ' + response.message() + ' ' + response.request().url() + " (" + requestDurationMs + "ms" + ')');
|
||||
}
|
||||
|
||||
private boolean hasBody(Request request) {
|
||||
return request.body() != null;
|
||||
}
|
||||
|
||||
private void logHeaders(Headers headers) {
|
||||
for (int i = 0, count = headers.size(); i < count; i++) {
|
||||
String name = headers.name(i);
|
||||
if (isExcludedHeader(name)) {
|
||||
continue;
|
||||
}
|
||||
logger.log(name + ": " + headers.value(i));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExcludedHeader(String name) {
|
||||
return EXCLUDED_HEADERS.contains(name);
|
||||
}
|
||||
|
||||
public interface Logger {
|
||||
|
||||
void log(String message);
|
||||
}
|
||||
}
|
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