Merge pull request #327 from cryptomator/feature/kotlin-refactoring
Kotlin Refactoring
This commit is contained in:
commit
d5436c9b47
@ -17,10 +17,10 @@ ext {
|
|||||||
|
|
||||||
// support lib
|
// support lib
|
||||||
androidSupportAnnotationsVersion = '1.2.0'
|
androidSupportAnnotationsVersion = '1.2.0'
|
||||||
androidSupportAppcompatVersion = '1.2.0'
|
androidSupportAppcompatVersion = '1.3.0'
|
||||||
androidSupportDesignVersion = '1.3.0'
|
androidSupportDesignVersion = '1.4.0'
|
||||||
|
|
||||||
coreDesugaringVersion = '1.0.9'
|
coreDesugaringVersion = '1.1.5'
|
||||||
|
|
||||||
// app frameworks and utilities
|
// app frameworks and utilities
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ ext {
|
|||||||
rxAndroidVersion = '2.1.1'
|
rxAndroidVersion = '2.1.1'
|
||||||
rxBindingVersion = '2.2.0'
|
rxBindingVersion = '2.2.0'
|
||||||
|
|
||||||
daggerVersion = '2.36'
|
daggerVersion = '2.37'
|
||||||
|
|
||||||
gsonVersion = '2.8.7'
|
gsonVersion = '2.8.7'
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ ext {
|
|||||||
|
|
||||||
timberVersion = '4.7.1'
|
timberVersion = '4.7.1'
|
||||||
|
|
||||||
zxcvbnVersion = '1.5.0'
|
zxcvbnVersion = '1.5.2'
|
||||||
|
|
||||||
scaleImageViewVersion = '3.10.0'
|
scaleImageViewVersion = '3.10.0'
|
||||||
|
|
||||||
@ -49,19 +49,17 @@ ext {
|
|||||||
greenDaoVersion = '3.3.0'
|
greenDaoVersion = '3.3.0'
|
||||||
|
|
||||||
// cloud provider libs
|
// cloud provider libs
|
||||||
|
cryptolibVersion = '2.0.0-rc6'
|
||||||
// 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-rc2'
|
|
||||||
|
|
||||||
dropboxVersion = '4.0.0'
|
dropboxVersion = '4.0.0'
|
||||||
|
|
||||||
googleApiServicesVersion = 'v3-rev197-1.25.0'
|
googleApiServicesVersion = 'v3-rev197-1.25.0'
|
||||||
googlePlayServicesVersion = '19.0.0'
|
googlePlayServicesVersion = '19.0.0'
|
||||||
googleClientVersion = '1.31.5'
|
googleClientVersion = '1.32.1'
|
||||||
|
|
||||||
msgraphVersion = '2.10.0'
|
msgraphVersion = '2.10.0'
|
||||||
|
|
||||||
minIoVersion = '8.2.1'
|
minIoVersion = '8.2.2'
|
||||||
staxVersion = '1.2.0' // needed for minIO
|
staxVersion = '1.2.0' // needed for minIO
|
||||||
|
|
||||||
commonsCodecVersion = '1.15'
|
commonsCodecVersion = '1.15'
|
||||||
@ -73,26 +71,26 @@ ext {
|
|||||||
jUnitVersion = '5.7.1'
|
jUnitVersion = '5.7.1'
|
||||||
jUnit4Version = '4.13.1'
|
jUnit4Version = '4.13.1'
|
||||||
assertJVersion = '1.7.1'
|
assertJVersion = '1.7.1'
|
||||||
mockitoVersion = '3.10.0'
|
mockitoVersion = '3.11.2'
|
||||||
mockitoInlineVersion = '3.10.0'
|
mockitoKotlinVersion = '3.2.0'
|
||||||
hamcrestVersion = '1.3'
|
hamcrestVersion = '1.3'
|
||||||
dexmakerVersion = '1.0'
|
dexmakerVersion = '1.0'
|
||||||
espressoVersion = '3.3.0'
|
espressoVersion = '3.4.0'
|
||||||
testingSupportLibVersion = '0.1'
|
testingSupportLibVersion = '0.1'
|
||||||
runnerVersion = '1.3.0'
|
runnerVersion = '1.4.0'
|
||||||
rulesVersion = '1.3.0'
|
rulesVersion = '1.4.0'
|
||||||
contributionVersion = '3.3.0'
|
contributionVersion = '3.4.0'
|
||||||
uiautomatorVersion = '2.2.0'
|
uiautomatorVersion = '2.2.0'
|
||||||
|
|
||||||
androidxCoreVersion = '1.3.2'
|
androidxCoreVersion = '1.6.0'
|
||||||
androidxFragmentVersion = '1.3.3'
|
androidxFragmentVersion = '1.3.5'
|
||||||
androidxViewpagerVersion = '1.0.0'
|
androidxViewpagerVersion = '1.0.0'
|
||||||
androidxSwiperefreshVersion = '1.1.0'
|
androidxSwiperefreshVersion = '1.1.0'
|
||||||
androidxPreferenceVersion = '1.1.1'
|
androidxPreferenceVersion = '1.1.1'
|
||||||
androidxRecyclerViewVersion = '1.2.0'
|
androidxRecyclerViewVersion = '1.2.1'
|
||||||
androidxDocumentfileVersion = '1.0.1'
|
androidxDocumentfileVersion = '1.0.1'
|
||||||
androidxBiometricVersion = '1.1.0'
|
androidxBiometricVersion = '1.1.0'
|
||||||
androidxTestCoreVersion = '1.3.0'
|
androidxTestCoreVersion = '1.4.0'
|
||||||
|
|
||||||
jsonWebTokenApiVersion = '0.11.2'
|
jsonWebTokenApiVersion = '0.11.2'
|
||||||
|
|
||||||
@ -132,7 +130,8 @@ ext {
|
|||||||
junit4Engine : "org.junit.vintage:junit-vintage-engine:${jUnitVersion}",
|
junit4Engine : "org.junit.vintage:junit-vintage-engine:${jUnitVersion}",
|
||||||
minIo : "io.minio:minio:${minIoVersion}",
|
minIo : "io.minio:minio:${minIoVersion}",
|
||||||
mockito : "org.mockito:mockito-core:${mockitoVersion}",
|
mockito : "org.mockito:mockito-core:${mockitoVersion}",
|
||||||
mockitoInline : "org.mockito:mockito-inline:${mockitoInlineVersion}",
|
mockitoInline : "org.mockito:mockito-inline:${mockitoVersion}",
|
||||||
|
mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:${mockitoKotlinVersion}",
|
||||||
msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}",
|
msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}",
|
||||||
multidex : "androidx.multidex:multidex:${multidexVersion}",
|
multidex : "androidx.multidex:multidex:${multidexVersion}",
|
||||||
okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}",
|
okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}",
|
||||||
|
@ -164,6 +164,7 @@ dependencies {
|
|||||||
testRuntimeOnly dependencies.junit4Engine
|
testRuntimeOnly dependencies.junit4Engine
|
||||||
|
|
||||||
testImplementation dependencies.mockito
|
testImplementation dependencies.mockito
|
||||||
|
testImplementation dependencies.mockitoKotlin
|
||||||
testImplementation dependencies.mockitoInline
|
testImplementation dependencies.mockitoInline
|
||||||
testImplementation dependencies.hamcrest
|
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 android.content.Context;
|
||||||
|
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
|
||||||
import org.cryptomator.cryptolib.api.Cryptor;
|
import org.cryptomator.cryptolib.api.Cryptor;
|
||||||
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
|
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
|
||||||
import org.cryptomator.domain.Cloud;
|
import org.cryptomator.domain.Cloud;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.MissingCryptorException;
|
import org.cryptomator.domain.exception.MissingCryptorException;
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@ -38,7 +39,7 @@ public class CryptoCloudContentRepositoryFactory implements CloudContentReposito
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
|
public CloudContentRepository<CryptoCloud, CryptoNode, CryptoFolder, CryptoFile> cloudContentRepositoryFor(Cloud cloud) {
|
||||||
CryptoCloud cryptoCloud = (CryptoCloud) cloud;
|
CryptoCloud cryptoCloud = (CryptoCloud) cloud;
|
||||||
Vault vault = cryptoCloud.getVault();
|
Vault vault = cryptoCloud.getVault();
|
||||||
return new CryptoCloudContentRepository(context, cloudContentRepository.get(), cryptoCloud, cryptors.get(vault));
|
return new CryptoCloudContentRepository(context, cloudContentRepository.get(), cryptoCloud, cryptors.get(vault));
|
||||||
@ -50,7 +51,7 @@ public class CryptoCloudContentRepositoryFactory implements CloudContentReposito
|
|||||||
|
|
||||||
public void deregisterCryptor(Vault vault, boolean assertPresent) {
|
public void deregisterCryptor(Vault vault, boolean assertPresent) {
|
||||||
Optional<Cryptor> cryptor = cryptors.remove(vault);
|
Optional<Cryptor> cryptor = cryptors.remove(vault);
|
||||||
if (cryptor.isAbsent()) {
|
if (!cryptor.isPresent()) {
|
||||||
if (assertPresent) {
|
if (assertPresent) {
|
||||||
throw new IllegalStateException(format("No cryptor registered for vault %s", vault));
|
throw new IllegalStateException(format("No cryptor registered for vault %s", vault));
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
package org.cryptomator.data.cloud.crypto;
|
||||||
|
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
import org.cryptomator.domain.Cloud;
|
||||||
import org.cryptomator.domain.CloudFile;
|
import org.cryptomator.domain.CloudFile;
|
||||||
import org.cryptomator.domain.CloudFolder;
|
import org.cryptomator.domain.CloudFolder;
|
||||||
|
import org.cryptomator.domain.CloudNode;
|
||||||
import org.cryptomator.domain.UnverifiedVaultConfig;
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||||
|
import org.cryptomator.domain.usecases.ProgressAware;
|
||||||
import org.cryptomator.domain.usecases.cloud.Flag;
|
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||||
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
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.MASTERKEY_SCHEME;
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME;
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME;
|
||||||
import static org.cryptomator.domain.Vault.aCopyOf;
|
import static org.cryptomator.domain.Vault.aCopyOf;
|
||||||
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
|
||||||
import static org.cryptomator.util.Encodings.UTF_8;
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class CryptoCloudFactory {
|
public class CryptoCloudFactory {
|
||||||
|
|
||||||
private final CloudContentRepository cloudContentRepository;
|
private final CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile> cloudContentRepository;
|
||||||
private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
|
private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
|
||||||
private final SecureRandom secureRandom = new SecureRandom();
|
private final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public CryptoCloudFactory(CloudContentRepository cloudContentRepository, //
|
public CryptoCloudFactory(CloudContentRepository/*<Cloud, CloudNode, CloudFolder, CloudFile>*/ cloudContentRepository, //
|
||||||
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) {
|
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) {
|
||||||
this.cloudContentRepository = cloudContentRepository;
|
this.cloudContentRepository = cloudContentRepository;
|
||||||
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
|
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void create(CloudFolder location, CharSequence password) throws BackendException {
|
public void create(CloudFolder location, CharSequence password) throws BackendException {
|
||||||
cryptoCloudProvider(Optional.empty()).create(location, password);
|
cryptoCloudProvider(Optional.absent()).create(location, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cloud decryptedViewOf(Vault vault) throws BackendException {
|
public Cloud decryptedViewOf(Vault vault) throws BackendException {
|
||||||
@ -47,14 +49,14 @@ public class CryptoCloudFactory {
|
|||||||
|
|
||||||
public Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException {
|
public Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException {
|
||||||
CloudFolder vaultLocation = cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
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));
|
return Optional.of(VaultConfig.decode(jwt));
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] readConfigFileData(CloudFolder location) throws BackendException {
|
private byte[] readConfigFileData(CloudFolder location) throws BackendException {
|
||||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||||
CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME);
|
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();
|
return data.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,13 +86,10 @@ public class CryptoCloudFactory {
|
|||||||
|
|
||||||
private CryptoCloudProvider cryptoCloudProvider(Optional<UnverifiedVaultConfig> unverifiedVaultConfigOptional) {
|
private CryptoCloudProvider cryptoCloudProvider(Optional<UnverifiedVaultConfig> unverifiedVaultConfigOptional) {
|
||||||
if (unverifiedVaultConfigOptional.isPresent()) {
|
if (unverifiedVaultConfigOptional.isPresent()) {
|
||||||
switch (unverifiedVaultConfigOptional.get().getKeyId().getScheme()) {
|
if (MASTERKEY_SCHEME.equals(unverifiedVaultConfigOptional.get().getKeyId().getScheme())) {
|
||||||
case MASTERKEY_SCHEME: {
|
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
|
||||||
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme()));
|
|
||||||
}
|
}
|
||||||
|
throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme()));
|
||||||
} else {
|
} else {
|
||||||
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
|
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
package org.cryptomator.data.cloud.crypto;
|
||||||
|
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
import org.cryptomator.domain.CloudFolder;
|
||||||
import org.cryptomator.domain.UnverifiedVaultConfig;
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.usecases.cloud.Flag;
|
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||||
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
public interface CryptoCloudProvider {
|
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
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import org.cryptomator.cryptolib.api.CryptorProvider
|
||||||
import org.cryptomator.domain.UnverifiedVaultConfig
|
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||||
import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException
|
import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException
|
||||||
import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException
|
import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException
|
||||||
@ -23,7 +24,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) {
|
|||||||
val keyId: URI
|
val keyId: URI
|
||||||
val id: String
|
val id: String
|
||||||
val vaultFormat: Int
|
val vaultFormat: Int
|
||||||
val cipherCombo: VaultCipherCombo
|
val cipherCombo: CryptorProvider.Scheme
|
||||||
val shorteningThreshold: Int
|
val shorteningThreshold: Int
|
||||||
|
|
||||||
fun toToken(rawKey: ByteArray): String {
|
fun toToken(rawKey: ByteArray): String {
|
||||||
@ -41,7 +42,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) {
|
|||||||
|
|
||||||
internal var id: String = UUID.randomUUID().toString()
|
internal var id: String = UUID.randomUUID().toString()
|
||||||
internal var vaultFormat = CryptoConstants.MAX_VAULT_VERSION;
|
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;
|
internal var shorteningThreshold = CryptoConstants.DEFAULT_MAX_FILE_NAME;
|
||||||
lateinit var keyId: URI
|
lateinit var keyId: URI
|
||||||
|
|
||||||
@ -50,7 +51,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cipherCombo(cipherCombo: VaultCipherCombo): VaultConfigBuilder {
|
fun cipherCombo(cipherCombo: CryptorProvider.Scheme): VaultConfigBuilder {
|
||||||
this.cipherCombo = cipherCombo
|
this.cipherCombo = cipherCombo
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@ -110,16 +111,15 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) {
|
|||||||
val vaultConfigBuilder = createVaultConfig() //
|
val vaultConfigBuilder = createVaultConfig() //
|
||||||
.keyId(unverifiedVaultConfig.keyId)
|
.keyId(unverifiedVaultConfig.keyId)
|
||||||
.id(parser.header[JSON_KEY_ID] as String) //
|
.id(parser.header[JSON_KEY_ID] as String) //
|
||||||
.cipherCombo(VaultCipherCombo.valueOf(parser.body.get(JSON_KEY_CIPHERCONFIG, String::class.java))) //
|
.cipherCombo(CryptorProvider.Scheme.valueOf(parser.body.get(JSON_KEY_CIPHERCONFIG, String::class.java))) //
|
||||||
.vaultFormat(unverifiedVaultConfig.vaultFormat) //
|
.vaultFormat(unverifiedVaultConfig.vaultFormat) //
|
||||||
.shorteningThreshold(parser.body[JSON_KEY_SHORTENING_THRESHOLD] as Int)
|
.shorteningThreshold(parser.body[JSON_KEY_SHORTENING_THRESHOLD] as Int)
|
||||||
|
|
||||||
VaultConfig(vaultConfigBuilder)
|
VaultConfig(vaultConfigBuilder)
|
||||||
} catch (e: Exception) {
|
} catch (e: JwtException) {
|
||||||
when (e) {
|
when (e) {
|
||||||
is MissingClaimException, is IncorrectClaimException -> throw VaultVersionMismatchException("Vault config not for version " + unverifiedVaultConfig.vaultFormat)
|
is MissingClaimException, is IncorrectClaimException -> throw VaultVersionMismatchException("Vault config not for version " + unverifiedVaultConfig.vaultFormat)
|
||||||
is SignatureException -> throw VaultKeyInvalidException()
|
is SignatureException -> throw VaultKeyInvalidException()
|
||||||
is JwtException -> throw VaultConfigLoadException("Failed to verify vault config", e)
|
|
||||||
else -> throw VaultConfigLoadException(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,205 @@
|
|||||||
|
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.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) {
|
||||||
|
if (ExceptionUtil.contains(e, DownloadErrorException::class.java)) {
|
||||||
|
if (ExceptionUtil.extract(e, DownloadErrorException::class.java).get().errorValue.pathValue.isNotFound) {
|
||||||
|
throw NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
if (ExceptionUtil.contains(e, DownloadErrorException::class.java)) {
|
||||||
|
if (ExceptionUtil.extract(e, DownloadErrorException::class.java).get().errorValue.pathValue.isNotFound) {
|
||||||
|
throw NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun delete(node: DropboxNode) {
|
||||||
|
try {
|
||||||
|
cloud.delete(node)
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
if (ExceptionUtil.contains(e, DeleteErrorException::class.java)) {
|
||||||
|
if (ExceptionUtil.extract(e, DeleteErrorException::class.java).get().errorValue.pathLookupValue.isNotFound) {
|
||||||
|
throw NoSuchCloudFileException(node.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: DropboxCloud): String {
|
||||||
|
return try {
|
||||||
|
this.cloud.currentAccount()
|
||||||
|
} catch (e: DbxException) {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun logout(cloud: DropboxCloud) {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,7 @@ public class DropboxCloudContentRepositoryFactory implements CloudContentReposit
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
|
public CloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile> cloudContentRepositoryFor(Cloud cloud) {
|
||||||
return new DropboxCloudContentRepository((DropboxCloud) cloud, context);
|
return new DropboxCloudContentRepository((DropboxCloud) cloud, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import com.dropbox.core.v2.files.FileMetadata;
|
|
||||||
import com.dropbox.core.v2.files.FolderMetadata;
|
|
||||||
import com.dropbox.core.v2.files.Metadata;
|
|
||||||
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
class DropboxCloudNodeFactory {
|
|
||||||
|
|
||||||
public static DropboxFile from(DropboxFolder parent, FileMetadata metadata) {
|
|
||||||
return new DropboxFile(parent, metadata.getName(), metadata.getPathDisplay(), Optional.ofNullable(metadata.getSize()), Optional.ofNullable(metadata.getServerModified()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DropboxFile file(DropboxFolder parent, String name, Optional<Long> size, String path) {
|
|
||||||
return new DropboxFile(parent, name, path, size, Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DropboxFolder from(DropboxFolder parent, FolderMetadata metadata) {
|
|
||||||
return new DropboxFolder(parent, metadata.getName(), getNodePath(parent, metadata.getName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getNodePath(DropboxFolder parent, String name) {
|
|
||||||
return parent.getPath() + "/" + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DropboxFolder folder(DropboxFolder parent, String name, String path) {
|
|
||||||
return new DropboxFolder(parent, name, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DropboxNode from(DropboxFolder parent, Metadata metadata) {
|
|
||||||
if (metadata instanceof FileMetadata) {
|
|
||||||
return from(parent, (FileMetadata) metadata);
|
|
||||||
} else {
|
|
||||||
return from(parent, (FolderMetadata) metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,39 @@
|
|||||||
|
package org.cryptomator.data.cloud.dropbox
|
||||||
|
|
||||||
|
import com.dropbox.core.v2.files.FileMetadata
|
||||||
|
import com.dropbox.core.v2.files.FolderMetadata
|
||||||
|
import com.dropbox.core.v2.files.Metadata
|
||||||
|
|
||||||
|
internal object DropboxCloudNodeFactory {
|
||||||
|
|
||||||
|
fun from(parent: DropboxFolder, metadata: FileMetadata): DropboxFile {
|
||||||
|
return DropboxFile(parent, metadata.name, metadata.pathDisplay, metadata.size, metadata.serverModified)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun file(parent: DropboxFolder, name: String, size: Long?, path: String): DropboxFile {
|
||||||
|
return DropboxFile(parent, name, path, size, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun from(parent: DropboxFolder, metadata: FolderMetadata): DropboxFolder {
|
||||||
|
return DropboxFolder(parent, metadata.name, getNodePath(parent, metadata.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNodePath(parent: DropboxFolder, name: String): String {
|
||||||
|
return parent.path + "/" + name
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun folder(parent: DropboxFolder?, name: String, path: String): DropboxFolder {
|
||||||
|
return DropboxFolder(parent, name, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun from(parent: DropboxFolder, metadata: Metadata): DropboxNode {
|
||||||
|
return if (metadata is FileMetadata) {
|
||||||
|
from(parent, metadata)
|
||||||
|
} else {
|
||||||
|
from(parent, metadata as FolderMetadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,55 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
class DropboxFile implements CloudFile, DropboxNode {
|
|
||||||
|
|
||||||
private final DropboxFolder parent;
|
|
||||||
private final String name;
|
|
||||||
private final String path;
|
|
||||||
private final Optional<Long> size;
|
|
||||||
private final Optional<Date> modified;
|
|
||||||
|
|
||||||
public DropboxFile(DropboxFolder parent, String name, String path, Optional<Long> size, Optional<Date> modified) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
this.size = size;
|
|
||||||
this.modified = modified;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return parent.getCloud();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFolder getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Long> getSize() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Date> getModified() {
|
|
||||||
return modified;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,11 @@
|
|||||||
|
package org.cryptomator.data.cloud.dropbox
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFile
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
internal class DropboxFile(override val parent: DropboxFolder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, DropboxNode {
|
||||||
|
|
||||||
|
override val cloud: Cloud?
|
||||||
|
get() = parent.cloud
|
||||||
|
}
|
@ -1,42 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
|
|
||||||
class DropboxFolder implements CloudFolder, DropboxNode {
|
|
||||||
|
|
||||||
private final DropboxFolder parent;
|
|
||||||
private final String name;
|
|
||||||
private final String path;
|
|
||||||
|
|
||||||
public DropboxFolder(DropboxFolder parent, String name, String path) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cloud getCloud() {
|
|
||||||
return parent.getCloud();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFolder getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DropboxFolder withCloud(Cloud cloud) {
|
|
||||||
return new DropboxFolder(parent.withCloud(cloud), name, path);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,14 @@
|
|||||||
|
package org.cryptomator.data.cloud.dropbox
|
||||||
|
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.CloudFolder
|
||||||
|
|
||||||
|
open class DropboxFolder(override val parent: DropboxFolder?, override val name: String, override val path: String) : CloudFolder, DropboxNode {
|
||||||
|
|
||||||
|
override val cloud: Cloud?
|
||||||
|
get() = parent?.cloud
|
||||||
|
|
||||||
|
override fun withCloud(cloud: Cloud?): DropboxFolder? {
|
||||||
|
return DropboxFolder(parent?.withCloud(cloud), name, path)
|
||||||
|
}
|
||||||
|
}
|
@ -1,466 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.dropbox;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.dropbox.core.DbxException;
|
|
||||||
import com.dropbox.core.NetworkIOException;
|
|
||||||
import com.dropbox.core.RetryException;
|
|
||||||
import com.dropbox.core.v2.DbxClientV2;
|
|
||||||
import com.dropbox.core.v2.files.CommitInfo;
|
|
||||||
import com.dropbox.core.v2.files.CreateFolderResult;
|
|
||||||
import com.dropbox.core.v2.files.FileMetadata;
|
|
||||||
import com.dropbox.core.v2.files.FolderMetadata;
|
|
||||||
import com.dropbox.core.v2.files.GetMetadataErrorException;
|
|
||||||
import com.dropbox.core.v2.files.ListFolderResult;
|
|
||||||
import com.dropbox.core.v2.files.Metadata;
|
|
||||||
import com.dropbox.core.v2.files.RelocationResult;
|
|
||||||
import com.dropbox.core.v2.files.UploadSessionCursor;
|
|
||||||
import com.dropbox.core.v2.files.UploadSessionFinishErrorException;
|
|
||||||
import com.dropbox.core.v2.files.UploadSessionLookupErrorException;
|
|
||||||
import com.dropbox.core.v2.files.WriteMode;
|
|
||||||
import com.dropbox.core.v2.users.FullAccount;
|
|
||||||
import com.tomclaw.cache.DiskLruCache;
|
|
||||||
|
|
||||||
import org.cryptomator.data.util.TransferredBytesAwareInputStream;
|
|
||||||
import org.cryptomator.data.util.TransferredBytesAwareOutputStream;
|
|
||||||
import org.cryptomator.domain.CloudFile;
|
|
||||||
import org.cryptomator.domain.CloudFolder;
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
import org.cryptomator.domain.DropboxCloud;
|
|
||||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
|
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
|
||||||
import org.cryptomator.domain.exception.authentication.AuthenticationException;
|
|
||||||
import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException;
|
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.DownloadState;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.Progress;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
import org.cryptomator.util.SharedPreferencesHandler;
|
|
||||||
import org.cryptomator.util.crypto.CredentialCryptor;
|
|
||||||
import org.cryptomator.util.file.LruFileCacheUtil;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
|
|
||||||
import static org.cryptomator.util.file.LruFileCacheUtil.Cache.DROPBOX;
|
|
||||||
import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache;
|
|
||||||
import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache;
|
|
||||||
|
|
||||||
class DropboxImpl {
|
|
||||||
|
|
||||||
private static final long CHUNKED_UPLOAD_CHUNK_SIZE = 8L << 20;
|
|
||||||
private static final int CHUNKED_UPLOAD_MAX_ATTEMPTS = 5;
|
|
||||||
|
|
||||||
private final DropboxClientFactory clientFactory = new DropboxClientFactory();
|
|
||||||
private final DropboxCloud cloud;
|
|
||||||
private final RootDropboxFolder root;
|
|
||||||
private final Context context;
|
|
||||||
private final SharedPreferencesHandler sharedPreferencesHandler;
|
|
||||||
|
|
||||||
private DiskLruCache diskLruCache;
|
|
||||||
|
|
||||||
DropboxImpl(DropboxCloud cloud, Context context) {
|
|
||||||
if (cloud.accessToken() == null) {
|
|
||||||
throw new NoAuthenticationProvidedException(cloud);
|
|
||||||
}
|
|
||||||
this.cloud = cloud;
|
|
||||||
this.root = new RootDropboxFolder(cloud);
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
sharedPreferencesHandler = new SharedPreferencesHandler(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sleepQuietly(long millis) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(millis);
|
|
||||||
} catch (InterruptedException ex) {
|
|
||||||
throw new FatalBackendException("Error uploading to Dropbox: interrupted during backoff.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private DbxClientV2 client() throws AuthenticationException {
|
|
||||||
return clientFactory.getClient(decrypt(cloud.accessToken()), context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String decrypt(String password) {
|
|
||||||
return CredentialCryptor //
|
|
||||||
.getInstance(context) //
|
|
||||||
.decrypt(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFolder root() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFolder resolve(String path) {
|
|
||||||
if (path.startsWith("/")) {
|
|
||||||
path = path.substring(1);
|
|
||||||
}
|
|
||||||
String[] names = path.split("/");
|
|
||||||
DropboxFolder folder = root;
|
|
||||||
for (String name : names) {
|
|
||||||
folder = folder(folder, name);
|
|
||||||
}
|
|
||||||
return folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFile file(CloudFolder folder, String name) {
|
|
||||||
return file(folder, name, Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFile file(CloudFolder folder, String name, Optional<Long> size) {
|
|
||||||
return DropboxCloudNodeFactory.file( //
|
|
||||||
(DropboxFolder) folder, //
|
|
||||||
name, //
|
|
||||||
size, //
|
|
||||||
folder.getPath() + '/' + name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFolder folder(CloudFolder folder, String name) {
|
|
||||||
return DropboxCloudNodeFactory.folder( //
|
|
||||||
(DropboxFolder) folder, //
|
|
||||||
name, //
|
|
||||||
folder.getPath() + '/' + name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean exists(CloudNode node) throws AuthenticationException, DbxException {
|
|
||||||
try {
|
|
||||||
Metadata metadata = client() //
|
|
||||||
.files() //
|
|
||||||
.getMetadata(node.getPath());
|
|
||||||
if (node instanceof CloudFolder) {
|
|
||||||
return metadata instanceof FolderMetadata;
|
|
||||||
} else {
|
|
||||||
return metadata instanceof FileMetadata;
|
|
||||||
}
|
|
||||||
} catch (GetMetadataErrorException e) {
|
|
||||||
if (e.errorValue.isPath()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<DropboxNode> list(CloudFolder folder) throws AuthenticationException, DbxException {
|
|
||||||
List<DropboxNode> result = new ArrayList<>();
|
|
||||||
ListFolderResult listFolderResult = null;
|
|
||||||
do {
|
|
||||||
if (listFolderResult == null) {
|
|
||||||
listFolderResult = client() //
|
|
||||||
.files() //
|
|
||||||
.listFolder(folder.getPath());
|
|
||||||
} else {
|
|
||||||
String cursor = listFolderResult.getCursor();
|
|
||||||
listFolderResult = client() //
|
|
||||||
.files() //
|
|
||||||
.listFolderContinue(cursor);
|
|
||||||
}
|
|
||||||
List<Metadata> entryMetadata = listFolderResult.getEntries();
|
|
||||||
for (Metadata metadata : entryMetadata) {
|
|
||||||
result.add(DropboxCloudNodeFactory.from( //
|
|
||||||
(DropboxFolder) folder, //
|
|
||||||
metadata));
|
|
||||||
}
|
|
||||||
} while (listFolderResult.getHasMore());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFolder create(CloudFolder folder) throws AuthenticationException, DbxException {
|
|
||||||
CreateFolderResult createFolderResult = client() //
|
|
||||||
.files() //
|
|
||||||
.createFolderV2(folder.getPath());
|
|
||||||
|
|
||||||
return DropboxCloudNodeFactory.from( //
|
|
||||||
(DropboxFolder) folder.getParent(), //
|
|
||||||
createFolderResult.getMetadata());
|
|
||||||
}
|
|
||||||
|
|
||||||
public CloudNode move(CloudNode source, CloudNode target) throws AuthenticationException, DbxException {
|
|
||||||
RelocationResult relocationResult = client() //
|
|
||||||
.files() //
|
|
||||||
.moveV2(source.getPath(), target.getPath());
|
|
||||||
|
|
||||||
return DropboxCloudNodeFactory.from( //
|
|
||||||
(DropboxFolder) target.getParent(), //
|
|
||||||
relocationResult.getMetadata());
|
|
||||||
}
|
|
||||||
|
|
||||||
public DropboxFile write(DropboxFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, long size) throws AuthenticationException, DbxException, IOException, CloudNodeAlreadyExistsException {
|
|
||||||
if (!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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,135 @@
|
|||||||
|
package org.cryptomator.data.cloud.okhttplogging
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.cryptomator.util.SharedPreferencesHandler
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Protocol
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class HttpLoggingInterceptor(private val logger: Logger, private val context: Context) : Interceptor {
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
return if (debugModeEnabled(context)) {
|
||||||
|
proceedWithLogging(chain)
|
||||||
|
} else {
|
||||||
|
chain.proceed(chain.request())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun proceedWithLogging(chain: Interceptor.Chain): Response {
|
||||||
|
val request: Request = chain.request()
|
||||||
|
logRequest(request, chain)
|
||||||
|
return getAndLogResponse(request, chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun logRequest(request: Request, chain: Interceptor.Chain) {
|
||||||
|
logRequestStart(request, chain)
|
||||||
|
logContentTypeAndLength(request)
|
||||||
|
logHeaders(request.headers)
|
||||||
|
logRequestEnd(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun getAndLogResponse(request: Request, chain: Interceptor.Chain): Response {
|
||||||
|
val startOfRequestMs = System.nanoTime()
|
||||||
|
val response = getResponseLoggingExceptions(request, chain)
|
||||||
|
val requestDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startOfRequestMs)
|
||||||
|
logResponse(response, requestDurationMs)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun getResponseLoggingExceptions(request: Request, chain: Interceptor.Chain): Response {
|
||||||
|
return try {
|
||||||
|
chain.proceed(request)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
logger.log("<-- HTTP FAILED: $e")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logResponse(response: Response, requestDurationMs: Long) {
|
||||||
|
logResponseStart(response, requestDurationMs)
|
||||||
|
logHeaders(response.headers)
|
||||||
|
logger.log("<-- END HTTP")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun logRequestStart(request: Request, chain: Interceptor.Chain) {
|
||||||
|
val connection = chain.connection()
|
||||||
|
val protocol = connection?.protocol() ?: Protocol.HTTP_1_1
|
||||||
|
val bodyLength = if (hasBody(request)) request.body?.contentLength().toString() + "-byte body" else "unknown length"
|
||||||
|
logger.log(String.format("--> %s %s %s (%s)", request.method, request.url, protocol, bodyLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun logContentTypeAndLength(request: Request) {
|
||||||
|
// 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)) {
|
||||||
|
val body = request.body
|
||||||
|
if (body?.contentType() != null) {
|
||||||
|
logger.log("Content-Type: " + body.contentType())
|
||||||
|
}
|
||||||
|
if (body?.contentLength() != -1L) {
|
||||||
|
logger.log("Content-Length: " + body?.contentLength())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logRequestEnd(request: Request) {
|
||||||
|
logger.log("--> END " + request.method)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logResponseStart(response: Response, requestDurationMs: Long) {
|
||||||
|
logger.log("<-- " + response.code + ' ' + response.message + ' ' + response.request.url + " (" + requestDurationMs + "ms" + ')')
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasBody(request: Request): Boolean {
|
||||||
|
return request.body != null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logHeaders(headers: Headers) {
|
||||||
|
var i = 0
|
||||||
|
while (i < headers.size) {
|
||||||
|
val name = headers.name(i)
|
||||||
|
if (isExcludedHeader(name)) {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.log(name + ": " + headers.value(i))
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isExcludedHeader(name: String): Boolean {
|
||||||
|
return EXCLUDED_HEADERS.contains(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Logger {
|
||||||
|
fun log(message: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val EXCLUDED_HEADERS = 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 fun debugModeEnabled(context: Context): Boolean {
|
||||||
|
return SharedPreferencesHandler(context).debugMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,76 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.onedrive;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.microsoft.graph.authentication.IAuthenticationProvider;
|
|
||||||
import com.microsoft.graph.core.DefaultClientConfig;
|
|
||||||
import com.microsoft.graph.models.extensions.IGraphServiceClient;
|
|
||||||
import com.microsoft.graph.requests.extensions.GraphServiceClient;
|
|
||||||
|
|
||||||
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor;
|
|
||||||
import org.cryptomator.data.cloud.onedrive.graph.IAuthenticationAdapter;
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
public class OnedriveClientFactory {
|
|
||||||
|
|
||||||
private static OnedriveClientFactory instance;
|
|
||||||
private final AtomicReference<IGraphServiceClient> graphServiceClient = new AtomicReference<>();
|
|
||||||
private final IAuthenticationAdapter authenticationAdapter;
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
private OnedriveClientFactory(Context context, String refreshToken) {
|
|
||||||
this.context = context;
|
|
||||||
this.authenticationAdapter = new MSAAuthAndroidAdapterImpl(context, refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OnedriveClientFactory instance(Context context, String accessToken) {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new OnedriveClientFactory(context, accessToken);
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Interceptor httpLoggingInterceptor(Context context) {
|
|
||||||
return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IGraphServiceClient client() {
|
|
||||||
if (graphServiceClient.get() == null) {
|
|
||||||
|
|
||||||
OkHttpClient.Builder builder = new OkHttpClient() //
|
|
||||||
.newBuilder() //
|
|
||||||
.connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) //
|
|
||||||
.readTimeout(READ.getTimeout(), READ.getUnit()) //
|
|
||||||
.writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) //
|
|
||||||
.addInterceptor(httpLoggingInterceptor(context));
|
|
||||||
|
|
||||||
OnedriveHttpProvider onedriveHttpProvider = new OnedriveHttpProvider(new DefaultClientConfig() {
|
|
||||||
@Override
|
|
||||||
public IAuthenticationProvider getAuthenticationProvider() {
|
|
||||||
return getAuthenticationAdapter();
|
|
||||||
}
|
|
||||||
}, builder.build());
|
|
||||||
|
|
||||||
graphServiceClient.set(GraphServiceClient //
|
|
||||||
.builder() //
|
|
||||||
.authenticationProvider(getAuthenticationAdapter()) //
|
|
||||||
.httpProvider(onedriveHttpProvider) //
|
|
||||||
.buildClient());
|
|
||||||
}
|
|
||||||
return graphServiceClient.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized IAuthenticationAdapter getAuthenticationAdapter() {
|
|
||||||
return authenticationAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,63 @@
|
|||||||
|
package org.cryptomator.data.cloud.onedrive
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.microsoft.graph.authentication.IAuthenticationProvider
|
||||||
|
import com.microsoft.graph.core.DefaultClientConfig
|
||||||
|
import com.microsoft.graph.models.extensions.IGraphServiceClient
|
||||||
|
import com.microsoft.graph.requests.extensions.GraphServiceClient
|
||||||
|
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor
|
||||||
|
import org.cryptomator.data.cloud.onedrive.graph.MSAAuthAndroidAdapter
|
||||||
|
import org.cryptomator.data.util.NetworkTimeout
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class OnedriveClientFactory private constructor() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var instance: IGraphServiceClient? = null
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var authenticationAdapter: MSAAuthAndroidAdapter? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getInstance(context: Context, refreshToken: String?): IGraphServiceClient = instance ?: createClient(context, refreshToken).also { instance = it }
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getAuthAdapter(context: Context, refreshToken: String?): MSAAuthAndroidAdapter = authenticationAdapter ?: MSAAuthAndroidAdapterImpl(context, refreshToken).also { authenticationAdapter = it }
|
||||||
|
|
||||||
|
private fun createClient(context: Context, refreshToken: String?): IGraphServiceClient {
|
||||||
|
val builder = 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))
|
||||||
|
|
||||||
|
val onedriveHttpProvider = OnedriveHttpProvider(object : DefaultClientConfig() {
|
||||||
|
override fun getAuthenticationProvider(): IAuthenticationProvider {
|
||||||
|
return getAuthAdapter(context, refreshToken)
|
||||||
|
}
|
||||||
|
}, builder.build())
|
||||||
|
|
||||||
|
return GraphServiceClient //
|
||||||
|
.builder() //
|
||||||
|
.authenticationProvider(authenticationAdapter) //
|
||||||
|
.httpProvider(onedriveHttpProvider) //
|
||||||
|
.buildClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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,165 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.onedrive;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.microsoft.graph.core.GraphErrorCodes;
|
|
||||||
|
|
||||||
import org.cryptomator.data.cloud.InterceptingCloudContentRepository;
|
|
||||||
import org.cryptomator.data.cloud.onedrive.graph.ClientException;
|
|
||||||
import org.cryptomator.domain.CloudNode;
|
|
||||||
import org.cryptomator.domain.OnedriveCloud;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
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.net.SocketTimeoutException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.cryptomator.util.ExceptionUtil.contains;
|
|
||||||
|
|
||||||
class OnedriveCloudContentRepository extends InterceptingCloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile> {
|
|
||||||
|
|
||||||
private final OnedriveCloud cloud;
|
|
||||||
|
|
||||||
public OnedriveCloudContentRepository(OnedriveCloud cloud, Context context) {
|
|
||||||
super(new Intercepted(cloud, context));
|
|
||||||
this.cloud = cloud;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void throwWrappedIfRequired(Exception e) throws BackendException {
|
|
||||||
throwNetworkConnectionExceptionIfRequired(e);
|
|
||||||
throwWrongCredentialsExceptionIfRequired(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void throwNetworkConnectionExceptionIfRequired(Exception e) throws NetworkConnectionException {
|
|
||||||
if (contains(e, SocketTimeoutException.class)) {
|
|
||||||
throw new NetworkConnectionException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void throwWrongCredentialsExceptionIfRequired(Exception e) {
|
|
||||||
if (isAuthenticationError(e)) {
|
|
||||||
throw new WrongCredentialsException(cloud);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAuthenticationError(Throwable e) {
|
|
||||||
return e != null //
|
|
||||||
&& ((e instanceof ClientException && ((ClientException) e).errorCode().equals(GraphErrorCodes.AUTHENTICATION_FAILURE)) //
|
|
||||||
|| isAuthenticationError(e.getCause()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Intercepted implements CloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile> {
|
|
||||||
|
|
||||||
private final OnedriveImpl oneDriveImpl;
|
|
||||||
|
|
||||||
public Intercepted(OnedriveCloud cloud, Context context) {
|
|
||||||
this.oneDriveImpl = new OnedriveImpl(cloud, context, new OnedriveIdCache());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OnedriveFolder root(OnedriveCloud cloud) {
|
|
||||||
return oneDriveImpl.root();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OnedriveFolder resolve(OnedriveCloud cloud, String path) {
|
|
||||||
return oneDriveImpl.resolve(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OnedriveFile file(OnedriveFolder parent, String name) {
|
|
||||||
return oneDriveImpl.file(parent, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OnedriveFile file(OnedriveFolder parent, String name, Optional<Long> size) {
|
|
||||||
return oneDriveImpl.file(parent, name, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OnedriveFolder folder(OnedriveFolder parent, String name) {
|
|
||||||
return oneDriveImpl.folder(parent, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean exists(OnedriveNode node) throws BackendException {
|
|
||||||
return oneDriveImpl.exists(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CloudNode> list(OnedriveFolder folder) throws BackendException {
|
|
||||||
return oneDriveImpl.list(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OnedriveFolder create(OnedriveFolder folder) throws BackendException {
|
|
||||||
return oneDriveImpl.create(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OnedriveFolder move(OnedriveFolder source, OnedriveFolder target) throws BackendException {
|
|
||||||
return (OnedriveFolder) oneDriveImpl.move(source, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OnedriveFile move(OnedriveFile source, OnedriveFile target) throws BackendException {
|
|
||||||
return (OnedriveFile) oneDriveImpl.move(source, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OnedriveFile write(OnedriveFile file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
|
|
||||||
try {
|
|
||||||
return oneDriveImpl.write(file, data, progressAware, replace, size);
|
|
||||||
} catch (BackendException e) {
|
|
||||||
if (contains(e, NoSuchCloudFileException.class)) {
|
|
||||||
throw new NoSuchCloudFileException(file.getName());
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void read(OnedriveFile file, Optional<File> tmpEncryptedFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
|
||||||
try {
|
|
||||||
oneDriveImpl.read(file, tmpEncryptedFile, data, progressAware);
|
|
||||||
} catch (IOException | BackendException e) {
|
|
||||||
if (contains(e, NoSuchCloudFileException.class)) {
|
|
||||||
throw new NoSuchCloudFileException(file.getName());
|
|
||||||
} else if (e instanceof IOException) {
|
|
||||||
throw new FatalBackendException(e);
|
|
||||||
} else if (e instanceof BackendException) {
|
|
||||||
throw (BackendException) e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(OnedriveNode node) throws BackendException {
|
|
||||||
oneDriveImpl.delete(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String checkAuthenticationAndRetrieveCurrentAccount(OnedriveCloud cloud) throws BackendException {
|
|
||||||
return oneDriveImpl.currentAccount();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void logout(OnedriveCloud cloud) {
|
|
||||||
oneDriveImpl.logout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,152 @@
|
|||||||
|
package org.cryptomator.data.cloud.onedrive
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.microsoft.graph.core.GraphErrorCodes
|
||||||
|
import org.cryptomator.data.cloud.InterceptingCloudContentRepository
|
||||||
|
import org.cryptomator.data.cloud.onedrive.graph.ClientException
|
||||||
|
import org.cryptomator.domain.OnedriveCloud
|
||||||
|
import org.cryptomator.domain.exception.BackendException
|
||||||
|
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
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
|
||||||
|
internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud, context: Context) : InterceptingCloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile>(Intercepted(cloud, context)) {
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun throwWrappedIfRequired(e: Exception) {
|
||||||
|
throwNetworkConnectionExceptionIfRequired(e)
|
||||||
|
throwWrongCredentialsExceptionIfRequired(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(NetworkConnectionException::class)
|
||||||
|
private fun throwNetworkConnectionExceptionIfRequired(e: Exception) {
|
||||||
|
if (ExceptionUtil.contains(e, SocketTimeoutException::class.java)) {
|
||||||
|
throw NetworkConnectionException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun throwWrongCredentialsExceptionIfRequired(e: Exception) {
|
||||||
|
if (isAuthenticationError(e)) {
|
||||||
|
throw WrongCredentialsException(cloud)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAuthenticationError(e: Throwable?): Boolean {
|
||||||
|
return (e != null //
|
||||||
|
&& (e is ClientException && e.errorCode() == GraphErrorCodes.AUTHENTICATION_FAILURE //
|
||||||
|
|| isAuthenticationError(e.cause)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Intercepted(cloud: OnedriveCloud, context: Context) : CloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile> {
|
||||||
|
|
||||||
|
private val oneDriveImpl: OnedriveImpl = OnedriveImpl(cloud, context, OnedriveIdCache())
|
||||||
|
|
||||||
|
override fun root(cloud: OnedriveCloud): OnedriveFolder {
|
||||||
|
return oneDriveImpl.root()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(cloud: OnedriveCloud, path: String): OnedriveFolder {
|
||||||
|
return oneDriveImpl.resolve(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun file(parent: OnedriveFolder, name: String): OnedriveFile {
|
||||||
|
return oneDriveImpl.file(parent, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun file(parent: OnedriveFolder, name: String, size: Long?): OnedriveFile {
|
||||||
|
return oneDriveImpl.file(parent, name, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun folder(parent: OnedriveFolder, name: String): OnedriveFolder {
|
||||||
|
return oneDriveImpl.folder(parent, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun exists(node: OnedriveNode): Boolean {
|
||||||
|
return oneDriveImpl.exists(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun list(folder: OnedriveFolder): List<OnedriveNode> {
|
||||||
|
return oneDriveImpl.list(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun create(folder: OnedriveFolder): OnedriveFolder {
|
||||||
|
return oneDriveImpl.create(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: OnedriveFolder, target: OnedriveFolder): OnedriveFolder {
|
||||||
|
return oneDriveImpl.move(source, target) as OnedriveFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun move(source: OnedriveFile, target: OnedriveFile): OnedriveFile {
|
||||||
|
return oneDriveImpl.move(source, target) as OnedriveFile
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun write(file: OnedriveFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): OnedriveFile {
|
||||||
|
return try {
|
||||||
|
oneDriveImpl.write(file, data, progressAware, replace, size)
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
if (ExceptionUtil.contains(e, NoSuchCloudFileException::class.java)) {
|
||||||
|
throw NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun read(file: OnedriveFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
|
||||||
|
try {
|
||||||
|
oneDriveImpl.read(file, encryptedTmpFile, data, progressAware)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
when {
|
||||||
|
ExceptionUtil.contains(e, NoSuchCloudFileException::class.java) -> {
|
||||||
|
throw NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw FatalBackendException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
when {
|
||||||
|
ExceptionUtil.contains(e, NoSuchCloudFileException::class.java) -> {
|
||||||
|
throw NoSuchCloudFileException(file.name)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun delete(node: OnedriveNode) {
|
||||||
|
oneDriveImpl.delete(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BackendException::class)
|
||||||
|
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: OnedriveCloud): String {
|
||||||
|
return oneDriveImpl.currentAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logout(cloud: OnedriveCloud) {
|
||||||
|
oneDriveImpl.logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,7 @@ public class OnedriveCloudContentRepositoryFactory implements CloudContentReposi
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
|
public CloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile> cloudContentRepositoryFor(Cloud cloud) {
|
||||||
return new OnedriveCloudContentRepository((OnedriveCloud) cloud, context);
|
return new OnedriveCloudContentRepository((OnedriveCloud) cloud, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
package org.cryptomator.data.cloud.onedrive;
|
|
||||||
|
|
||||||
import com.microsoft.graph.models.extensions.DriveItem;
|
|
||||||
|
|
||||||
import org.cryptomator.util.Optional;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
class OnedriveCloudNodeFactory {
|
|
||||||
|
|
||||||
public static OnedriveNode from(OnedriveFolder parent, DriveItem item) {
|
|
||||||
if (isFolder(item)) {
|
|
||||||
return folder(parent, item);
|
|
||||||
} else {
|
|
||||||
return file(parent, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static OnedriveFile file(OnedriveFolder parent, DriveItem item) {
|
|
||||||
return new OnedriveFile(parent, item.name, getNodePath(parent, item.name), Optional.ofNullable(item.size), lastModified(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OnedriveFile file(OnedriveFolder parent, DriveItem item, Optional<Date> lastModified) {
|
|
||||||
return new OnedriveFile(parent, item.name, getNodePath(parent, item.name), Optional.ofNullable(item.size), lastModified);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OnedriveFile file(OnedriveFolder parent, String name, Optional<Long> size) {
|
|
||||||
return new OnedriveFile(parent, name, getNodePath(parent, name), size, Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OnedriveFile file(OnedriveFolder parent, String name, Optional<Long> size, String path) {
|
|
||||||
return new OnedriveFile(parent, name, path, size, Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OnedriveFolder folder(OnedriveFolder parent, DriveItem item) {
|
|
||||||
return new OnedriveFolder(parent, item.name, getNodePath(parent, item.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OnedriveFolder folder(OnedriveFolder parent, String name) {
|
|
||||||
return new OnedriveFolder(parent, name, getNodePath(parent, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OnedriveFolder folder(OnedriveFolder parent, String name, String path) {
|
|
||||||
return new OnedriveFolder(parent, name, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getNodePath(OnedriveFolder parent, String name) {
|
|
||||||
return parent.getPath() + "/" + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getId(DriveItem item) {
|
|
||||||
return item.remoteItem != null //
|
|
||||||
? item.remoteItem.id //
|
|
||||||
: item.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getDriveId(DriveItem item) {
|
|
||||||
return item.remoteItem != null //
|
|
||||||
? item.remoteItem.parentReference.driveId //
|
|
||||||
: item.parentReference != null //
|
|
||||||
? item.parentReference.driveId //
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isFolder(DriveItem item) {
|
|
||||||
return item.folder != null || (item.remoteItem != null && item.remoteItem.folder != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<Date> lastModified(DriveItem item) {
|
|
||||||
if (item.lastModifiedDateTime == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
} else {
|
|
||||||
return Optional.of(item.lastModifiedDateTime.getTime());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
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