Merge branch 'release/1.5.18'
@ -42,7 +42,7 @@ allprojects {
|
||||
ext {
|
||||
androidApplicationId = 'org.cryptomator'
|
||||
androidVersionCode = getVersionCode()
|
||||
androidVersionName = '1.5.17'
|
||||
androidVersionName = '1.5.18'
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
@ -26,7 +26,7 @@ ext {
|
||||
rxAndroidVersion = '2.1.1'
|
||||
rxBindingVersion = '2.2.0'
|
||||
|
||||
daggerVersion = '2.34.1'
|
||||
daggerVersion = '2.35'
|
||||
|
||||
gsonVersion = '2.8.6'
|
||||
|
||||
@ -37,7 +37,7 @@ ext {
|
||||
|
||||
timberVersion = '4.7.1'
|
||||
|
||||
zxcvbnVersion = '1.4.1'
|
||||
zxcvbnVersion = '1.5.0'
|
||||
|
||||
scaleImageViewVersion = '3.10.0'
|
||||
|
||||
@ -51,6 +51,8 @@ ext {
|
||||
// do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x
|
||||
cryptolibVersion = '1.3.0'
|
||||
|
||||
awsAndroidSdkS3 = '2.23.0'
|
||||
|
||||
dropboxVersion = '4.0.0'
|
||||
|
||||
googleApiServicesVersion = 'v3-rev197-1.25.0'
|
||||
@ -101,6 +103,7 @@ ext {
|
||||
androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}",
|
||||
androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}",
|
||||
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
|
||||
awsAndroidS3 : "com.amazonaws:aws-android-sdk-s3:${awsAndroidSdkS3}",
|
||||
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
|
||||
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
|
||||
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",
|
||||
|
@ -74,7 +74,7 @@ android {
|
||||
}
|
||||
|
||||
greendao {
|
||||
schemaVersion 5
|
||||
schemaVersion 6
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
@ -99,6 +99,7 @@ dependencies {
|
||||
annotationProcessor dependencies.daggerCompiler
|
||||
implementation dependencies.dagger
|
||||
// cloud
|
||||
implementation dependencies.awsAndroidS3
|
||||
implementation dependencies.dropbox
|
||||
implementation dependencies.msgraph
|
||||
|
||||
|
@ -5,6 +5,7 @@ import org.cryptomator.data.cloud.dropbox.DropboxCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.pcloud.PCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -25,6 +26,7 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
|
||||
public CloudContentRepositoryFactories(DropboxCloudContentRepositoryFactory dropboxFactory, //
|
||||
OnedriveCloudContentRepositoryFactory oneDriveFactory, //
|
||||
PCloudContentRepositoryFactory pCloudFactory, //
|
||||
S3CloudContentRepositoryFactory s3Factory, //
|
||||
CryptoCloudContentRepositoryFactory cryptoFactory, //
|
||||
LocalStorageContentRepositoryFactory localStorageFactory, //
|
||||
WebDavCloudContentRepositoryFactory webDavFactory) {
|
||||
@ -32,6 +34,7 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
|
||||
factories = asList(dropboxFactory, //
|
||||
oneDriveFactory, //
|
||||
pCloudFactory, //
|
||||
s3Factory, //
|
||||
cryptoFactory, //
|
||||
localStorageFactory, //
|
||||
webDavFactory);
|
||||
|
@ -16,8 +16,8 @@ class PCloudNodeFactory {
|
||||
return new PCloudFile(parent, name, getNodePath(parent, name), size, Optional.empty());
|
||||
}
|
||||
|
||||
public static PCloudFile file(PCloudFolder folder, String name, Optional<Long> size, String path) {
|
||||
return new PCloudFile(folder, name, path, size, Optional.empty());
|
||||
public static PCloudFile file(PCloudFolder parent, String name, Optional<Long> size, String path) {
|
||||
return new PCloudFile(parent, name, path, size, Optional.empty());
|
||||
}
|
||||
|
||||
public static PCloudFolder folder(PCloudFolder parent, RemoteFolder folder) {
|
||||
|
@ -0,0 +1,29 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import org.cryptomator.domain.Cloud;
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
|
||||
class RootS3Folder extends S3Folder {
|
||||
|
||||
private final S3Cloud cloud;
|
||||
|
||||
public RootS3Folder(S3Cloud cloud) {
|
||||
super(null, "", "");
|
||||
this.cloud = cloud;
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Cloud getCloud() {
|
||||
return cloud;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder withCloud(Cloud cloud) {
|
||||
return new RootS3Folder((S3Cloud) cloud);
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.amazonaws.Request;
|
||||
import com.amazonaws.Response;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.handlers.RequestHandler2;
|
||||
import com.amazonaws.regions.Region;
|
||||
import com.amazonaws.regions.Regions;
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.amazonaws.services.s3.AmazonS3Client;
|
||||
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
import org.cryptomator.util.crypto.CredentialCryptor;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
class S3ClientFactory {
|
||||
|
||||
private AmazonS3 apiClient;
|
||||
|
||||
public AmazonS3 getClient(S3Cloud cloud, Context context) {
|
||||
if (apiClient == null) {
|
||||
apiClient = createApiClient(cloud, context);
|
||||
}
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
private AmazonS3 createApiClient(S3Cloud cloud, Context context) {
|
||||
Region region = Region.getRegion(Regions.DEFAULT_REGION);
|
||||
String endpoint = null;
|
||||
|
||||
if (cloud.s3Region() != null) {
|
||||
region = Region.getRegion(cloud.s3Region());
|
||||
} else if (cloud.s3Endpoint() != null) {
|
||||
endpoint = cloud.s3Endpoint();
|
||||
}
|
||||
|
||||
AmazonS3Client client = new AmazonS3Client(new BasicAWSCredentials(decrypt(cloud.accessKey(), context), decrypt(cloud.secretKey(), context)), region);
|
||||
|
||||
if (endpoint != null) {
|
||||
client.setEndpoint(cloud.s3Endpoint());
|
||||
}
|
||||
|
||||
client.addRequestHandler(new LoggingAwareRequestHandler());
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private String decrypt(String password, Context context) {
|
||||
return CredentialCryptor //
|
||||
.getInstance(context) //
|
||||
.decrypt(password);
|
||||
}
|
||||
|
||||
private static class LoggingAwareRequestHandler extends RequestHandler2 {
|
||||
|
||||
@Override
|
||||
public void beforeRequest(Request<?> request) {
|
||||
Timber.tag("S3Client").d("Sending request (%s) %s", request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), request.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterResponse(Request<?> request, Response<?> response) {
|
||||
Timber.tag("S3Client").d( //
|
||||
"Response received (%s) with status %s (%s)", //
|
||||
request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), //
|
||||
response.getHttpResponse().getStatusText(), //
|
||||
response.getHttpResponse().getStatusCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterError(Request<?> request, Response<?> response, Exception e) {
|
||||
if (response != null) {
|
||||
Timber.tag("S3Client").e( //
|
||||
e, //
|
||||
"Error occurred (%s) with status %s (%s)", //
|
||||
request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), //
|
||||
response.getHttpResponse().getStatusText(), //
|
||||
response.getHttpResponse().getStatusCode());
|
||||
} else {
|
||||
Timber.tag("S3Client").e(e, "Error occurred (%s)", request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
public enum S3CloudApiErrorCodes {
|
||||
ACCESS_DENIED("AccessDenied"),
|
||||
ACCOUNT_PROBLEM("AccountProblem"),
|
||||
INTERNAL_ERROR("InternalError"),
|
||||
INVALID_ACCESS_KEY_ID("InvalidAccessKeyId"),
|
||||
INVALID_BUCKET_NAME("InvalidBucketName"),
|
||||
INVALID_OBJECT_STATE("InvalidObjectState"),
|
||||
NO_SUCH_BUCKET("NoSuchBucket"),
|
||||
NO_SUCH_KEY("NoSuchKey");
|
||||
|
||||
private final String value;
|
||||
|
||||
S3CloudApiErrorCodes(final String newValue) {
|
||||
value = newValue;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
public class S3CloudApiExceptions {
|
||||
|
||||
public static boolean isAccessProblem(String errorCode) {
|
||||
return errorCode.equals(S3CloudApiErrorCodes.ACCESS_DENIED.getValue())
|
||||
|| errorCode.equals(S3CloudApiErrorCodes.ACCOUNT_PROBLEM.getValue())
|
||||
|| errorCode.equals(S3CloudApiErrorCodes.INVALID_ACCESS_KEY_ID.getValue());
|
||||
}
|
||||
|
||||
public static boolean isNoSuchBucketException(String errorCode) {
|
||||
return errorCode.equals(S3CloudApiErrorCodes.NO_SUCH_BUCKET.getValue());
|
||||
}
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.amazonaws.services.s3.model.AmazonS3Exception;
|
||||
|
||||
import org.cryptomator.data.cloud.InterceptingCloudContentRepository;
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
import org.cryptomator.domain.exception.BackendException;
|
||||
import org.cryptomator.domain.exception.FatalBackendException;
|
||||
import org.cryptomator.domain.exception.NetworkConnectionException;
|
||||
import org.cryptomator.domain.exception.NoSuchBucketException;
|
||||
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;
|
||||
|
||||
class S3CloudContentRepository extends InterceptingCloudContentRepository<S3Cloud, S3Node, S3Folder, S3File> {
|
||||
|
||||
private final S3Cloud cloud;
|
||||
|
||||
public S3CloudContentRepository(S3Cloud cloud, Context context) {
|
||||
super(new Intercepted(cloud, context));
|
||||
this.cloud = cloud;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void throwWrappedIfRequired(Exception e) throws BackendException {
|
||||
throwNoSuchBucketExceptionIfRequired(e);
|
||||
throwConnectionErrorIfRequired(e);
|
||||
throwWrongCredentialsExceptionIfRequired(e);
|
||||
}
|
||||
|
||||
private void throwNoSuchBucketExceptionIfRequired(Exception e) throws NoSuchBucketException {
|
||||
if (e instanceof AmazonS3Exception) {
|
||||
String errorCode = ((AmazonS3Exception)e).getErrorCode();
|
||||
if(S3CloudApiExceptions.isNoSuchBucketException(errorCode)) {
|
||||
throw new NoSuchBucketException(cloud.s3Bucket());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException {
|
||||
if (contains(e, IOException.class)) {
|
||||
throw new NetworkConnectionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void throwWrongCredentialsExceptionIfRequired(Exception e) {
|
||||
if (e instanceof AmazonS3Exception) {
|
||||
String errorCode = ((AmazonS3Exception) e).getErrorCode();
|
||||
if (S3CloudApiExceptions.isAccessProblem(errorCode)) {
|
||||
throw new WrongCredentialsException(cloud);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Intercepted implements CloudContentRepository<S3Cloud, S3Node, S3Folder, S3File> {
|
||||
|
||||
private final S3Impl cloud;
|
||||
|
||||
public Intercepted(S3Cloud cloud, Context context) {
|
||||
this.cloud = new S3Impl(context, cloud);
|
||||
}
|
||||
|
||||
public S3Folder root(S3Cloud cloud) {
|
||||
return this.cloud.root();
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder resolve(S3Cloud cloud, String path) throws BackendException {
|
||||
return this.cloud.resolve(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3File file(S3Folder parent, String name) throws BackendException {
|
||||
try {
|
||||
return cloud.file(parent, name);
|
||||
} catch (IOException ex) {
|
||||
throw new FatalBackendException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3File file(S3Folder parent, String name, Optional<Long> size) throws BackendException {
|
||||
try {
|
||||
return cloud.file(parent, name, size);
|
||||
} catch (IOException ex) {
|
||||
throw new FatalBackendException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder folder(S3Folder parent, String name) throws BackendException {
|
||||
return cloud.folder(parent, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(S3Node node) throws BackendException {
|
||||
return cloud.exists(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<S3Node> list(S3Folder folder) throws BackendException {
|
||||
try {
|
||||
return cloud.list(folder);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder create(S3Folder folder) throws BackendException {
|
||||
try {
|
||||
return cloud.create(folder);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder move(S3Folder source, S3Folder target) throws BackendException {
|
||||
try {
|
||||
return (S3Folder) cloud.move(source, target);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3File move(S3File source, S3File target) throws BackendException {
|
||||
try {
|
||||
return (S3File) cloud.move(source, target);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3File write(S3File uploadFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
|
||||
try {
|
||||
return cloud.write(uploadFile, data, progressAware, replace, size);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(S3File file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
||||
try {
|
||||
cloud.read(file, encryptedTmpFile, data, progressAware);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(S3Node node) throws BackendException {
|
||||
try {
|
||||
cloud.delete(node);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String checkAuthenticationAndRetrieveCurrentAccount(S3Cloud cloud) throws BackendException {
|
||||
return this.cloud.checkAuthenticationAndRetrieveCurrentAccount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(S3Cloud cloud) throws BackendException {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
|
||||
import org.cryptomator.domain.Cloud;
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import static org.cryptomator.domain.CloudType.S3;
|
||||
|
||||
@Singleton
|
||||
public class S3CloudContentRepositoryFactory implements CloudContentRepositoryFactory {
|
||||
|
||||
private final Context context;
|
||||
|
||||
@Inject
|
||||
public S3CloudContentRepositoryFactory(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Cloud cloud) {
|
||||
return cloud.type() == S3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
|
||||
return new S3CloudContentRepository((S3Cloud) cloud, context);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import com.amazonaws.services.s3.model.ObjectMetadata;
|
||||
import com.amazonaws.services.s3.model.S3ObjectSummary;
|
||||
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
class S3CloudNodeFactory {
|
||||
|
||||
private static final String DELIMITER = "/";
|
||||
|
||||
public static S3File file(S3Folder parent, S3ObjectSummary file) {
|
||||
String name = getNameFromKey(file.getKey());
|
||||
return new S3File(parent, name, getNodePath(parent, name), Optional.ofNullable(file.getSize()), Optional.ofNullable(file.getLastModified()));
|
||||
}
|
||||
|
||||
public static S3File file(S3Folder parent, String name, ObjectMetadata file) {
|
||||
return new S3File(parent, name, getNodePath(parent, name), Optional.ofNullable(file.getContentLength()), Optional.ofNullable(file.getLastModified()));
|
||||
}
|
||||
|
||||
|
||||
public static S3File file(S3Folder parent, String name, Optional<Long> size) {
|
||||
return new S3File(parent, name, getNodePath(parent, name), size, Optional.empty());
|
||||
}
|
||||
|
||||
public static S3File file(S3Folder parent, String name, Optional<Long> size, String path) {
|
||||
return new S3File(parent, name, path, size, Optional.empty());
|
||||
}
|
||||
|
||||
public static S3File file(S3Folder parent, String name, Optional<Long> size, Optional<Date> lastModified) {
|
||||
return new S3File(parent, name, getNodePath(parent, name), size, lastModified);
|
||||
}
|
||||
|
||||
public static S3Folder folder(S3Folder parent, S3ObjectSummary folder) {
|
||||
String name = getNameFromKey(folder.getKey());
|
||||
return new S3Folder(parent, name, getNodePath(parent, name));
|
||||
}
|
||||
|
||||
public static S3Folder folder(S3Folder parent, String name) {
|
||||
return new S3Folder(parent, name, getNodePath(parent, name));
|
||||
}
|
||||
|
||||
public static S3Folder folder(S3Folder parent, String name, String path) {
|
||||
return new S3Folder(parent, name, path);
|
||||
}
|
||||
|
||||
private static String getNodePath(S3Folder parent, String name) {
|
||||
return parent.getKey() + name;
|
||||
}
|
||||
|
||||
public static String getNameFromKey(String key) {
|
||||
String name = key;
|
||||
if (key.endsWith(DELIMITER)) {
|
||||
name = key.substring(0, key.length() -1);
|
||||
}
|
||||
return name.contains(DELIMITER) ? name.substring(name.lastIndexOf(DELIMITER) + 1) : name;
|
||||
}
|
||||
|
||||
public static S3Node from(S3Folder parent, S3ObjectSummary objectSummary) {
|
||||
if (objectSummary.getKey().endsWith(DELIMITER)) {
|
||||
return folder(parent, objectSummary);
|
||||
} else {
|
||||
return file(parent, objectSummary);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
60
data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java
Normal file
@ -0,0 +1,60 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import org.cryptomator.domain.Cloud;
|
||||
import org.cryptomator.domain.CloudFile;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
class S3File implements CloudFile, S3Node {
|
||||
|
||||
private final S3Folder parent;
|
||||
private final String name;
|
||||
private final String path;
|
||||
private final Optional<Long> size;
|
||||
private final Optional<Date> modified;
|
||||
|
||||
public S3File(S3Folder 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 String getKey() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Long> getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Date> getModified() {
|
||||
return modified;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import org.cryptomator.domain.Cloud;
|
||||
import org.cryptomator.domain.CloudFolder;
|
||||
|
||||
class S3Folder implements CloudFolder, S3Node {
|
||||
|
||||
private static final String DELIMITER = "/";
|
||||
|
||||
private final S3Folder parent;
|
||||
private final String name;
|
||||
private final String path;
|
||||
|
||||
public S3Folder(S3Folder 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 String getKey() {
|
||||
return path + DELIMITER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder withCloud(Cloud cloud) {
|
||||
return new S3Folder(parent.withCloud(cloud), name, path);
|
||||
}
|
||||
}
|
417
data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java
Normal file
@ -0,0 +1,417 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.amazonaws.event.ProgressListener;
|
||||
import com.amazonaws.mobileconnectors.s3.transferutility.TransferListener;
|
||||
import com.amazonaws.mobileconnectors.s3.transferutility.TransferState;
|
||||
import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility;
|
||||
import com.amazonaws.mobileconnectors.s3.transferutility.UploadOptions;
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.amazonaws.services.s3.model.AmazonS3Exception;
|
||||
import com.amazonaws.services.s3.model.CopyObjectResult;
|
||||
import com.amazonaws.services.s3.model.DeleteObjectsRequest;
|
||||
import com.amazonaws.services.s3.model.DeleteObjectsRequest.KeyVersion;
|
||||
import com.amazonaws.services.s3.model.GetObjectRequest;
|
||||
import com.amazonaws.services.s3.model.ListObjectsV2Request;
|
||||
import com.amazonaws.services.s3.model.ListObjectsV2Result;
|
||||
import com.amazonaws.services.s3.model.ObjectMetadata;
|
||||
import com.amazonaws.services.s3.model.Owner;
|
||||
import com.amazonaws.services.s3.model.PutObjectRequest;
|
||||
import com.amazonaws.services.s3.model.S3Object;
|
||||
import com.amazonaws.services.s3.model.S3ObjectSummary;
|
||||
import com.tomclaw.cache.DiskLruCache;
|
||||
|
||||
import org.cryptomator.data.util.CopyStream;
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
import org.cryptomator.domain.exception.BackendException;
|
||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
|
||||
import org.cryptomator.domain.exception.FatalBackendException;
|
||||
import org.cryptomator.domain.exception.ForbiddenException;
|
||||
import org.cryptomator.domain.exception.NoSuchBucketException;
|
||||
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
||||
import org.cryptomator.domain.exception.UnauthorizedException;
|
||||
import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
|
||||
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.concurrent.CompletableFuture;
|
||||
import org.cryptomator.util.file.LruFileCacheUtil;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
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 java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
|
||||
import static org.cryptomator.util.file.LruFileCacheUtil.Cache.S3;
|
||||
import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache;
|
||||
import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache;
|
||||
|
||||
class S3Impl {
|
||||
|
||||
private static final long CHUNKED_UPLOAD_MAX_SIZE = 100L << 20;
|
||||
private static final String DELIMITER = "/";
|
||||
|
||||
private final S3ClientFactory clientFactory = new S3ClientFactory();
|
||||
private final S3Cloud cloud;
|
||||
private final RootS3Folder root;
|
||||
private final Context context;
|
||||
|
||||
private final SharedPreferencesHandler sharedPreferencesHandler;
|
||||
private DiskLruCache diskLruCache;
|
||||
|
||||
S3Impl(Context context, S3Cloud cloud) {
|
||||
if (cloud.accessKey() == null || cloud.secretKey() == null) {
|
||||
throw new WrongCredentialsException(cloud);
|
||||
}
|
||||
|
||||
this.context = context;
|
||||
this.cloud = cloud;
|
||||
this.root = new RootS3Folder(cloud);
|
||||
this.sharedPreferencesHandler = new SharedPreferencesHandler(context);
|
||||
}
|
||||
|
||||
private AmazonS3 client() {
|
||||
return clientFactory.getClient(cloud, context);
|
||||
}
|
||||
|
||||
public S3Folder root() {
|
||||
return root;
|
||||
}
|
||||
|
||||
public S3Folder resolve(String path) {
|
||||
if (path.startsWith(DELIMITER)) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
String[] names = path.split(DELIMITER);
|
||||
S3Folder folder = root;
|
||||
for (String name : names) {
|
||||
if (!name.isEmpty()) {
|
||||
folder = folder(folder, name);
|
||||
}
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
public S3File file(S3Folder parent, String name) throws BackendException, IOException {
|
||||
return file(parent, name, Optional.empty());
|
||||
}
|
||||
|
||||
public S3File file(S3Folder parent, String name, Optional<Long> size) throws BackendException, IOException {
|
||||
return S3CloudNodeFactory.file(parent, name, size, parent.getKey() + name);
|
||||
}
|
||||
|
||||
public S3Folder folder(S3Folder parent, String name) {
|
||||
return S3CloudNodeFactory.folder(parent, name, parent.getKey() + name);
|
||||
}
|
||||
|
||||
public boolean exists(S3Node node) {
|
||||
String key = node.getKey();
|
||||
|
||||
ListObjectsV2Result result = client().listObjectsV2(cloud.s3Bucket(), key);
|
||||
|
||||
return result.getObjectSummaries().size() > 0;
|
||||
}
|
||||
|
||||
public List<S3Node> list(S3Folder folder) throws IOException, BackendException {
|
||||
List<S3Node> result = new ArrayList<>();
|
||||
|
||||
ListObjectsV2Request request = new ListObjectsV2Request().withBucketName(cloud.s3Bucket()).withPrefix(folder.getKey()).withDelimiter(DELIMITER);
|
||||
|
||||
ListObjectsV2Result listObjects = client().listObjectsV2(request);
|
||||
for (String prefix : listObjects.getCommonPrefixes()) {
|
||||
// add folders
|
||||
result.add(S3CloudNodeFactory.folder(folder, S3CloudNodeFactory.getNameFromKey(prefix)));
|
||||
}
|
||||
|
||||
for (S3ObjectSummary objectSummary : listObjects.getObjectSummaries()) {
|
||||
// add files but skip parent folder
|
||||
if (!objectSummary.getKey().equals(listObjects.getPrefix())) {
|
||||
result.add(S3CloudNodeFactory.file(folder, objectSummary));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public S3Folder create(S3Folder folder) throws IOException, BackendException {
|
||||
if (!exists(folder.getParent())) {
|
||||
folder = new S3Folder( //
|
||||
create(folder.getParent()), //
|
||||
folder.getName(), folder.getPath() //
|
||||
);
|
||||
}
|
||||
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
metadata.setContentLength(0);
|
||||
|
||||
InputStream emptyContent = new ByteArrayInputStream(new byte[0]);
|
||||
|
||||
try {
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(cloud.s3Bucket(), folder.getKey(), emptyContent, metadata);
|
||||
client().putObject(putObjectRequest);
|
||||
} catch(AmazonS3Exception ex) {
|
||||
handleApiError(ex, folder.getName());
|
||||
}
|
||||
|
||||
return S3CloudNodeFactory.folder(folder.getParent(), folder.getName());
|
||||
}
|
||||
|
||||
public S3Node move(S3Node source, S3Node target) throws IOException, BackendException {
|
||||
if (exists(target)) {
|
||||
throw new CloudNodeAlreadyExistsException(target.getName());
|
||||
}
|
||||
|
||||
if (source instanceof S3Folder) {
|
||||
ListObjectsV2Result listObjects = client().listObjectsV2(cloud.s3Bucket(), source.getPath());
|
||||
|
||||
if (listObjects.getObjectSummaries().size() > 0) {
|
||||
|
||||
List<DeleteObjectsRequest.KeyVersion> objectsToDelete = new ArrayList<>();
|
||||
|
||||
for (S3ObjectSummary summary : listObjects.getObjectSummaries()) {
|
||||
objectsToDelete.add(new DeleteObjectsRequest.KeyVersion(summary.getKey()));
|
||||
String destinationKey = summary.getKey().replace(source.getPath(), target.getPath());
|
||||
|
||||
client().copyObject(cloud.s3Bucket(), summary.getKey(), cloud.s3Bucket(), destinationKey);
|
||||
}
|
||||
client().deleteObjects(new DeleteObjectsRequest(cloud.s3Bucket()).withKeys(objectsToDelete));
|
||||
} else {
|
||||
throw new NoSuchCloudFileException(source.getPath());
|
||||
}
|
||||
return S3CloudNodeFactory.folder(target.getParent(), target.getName());
|
||||
} else {
|
||||
CopyObjectResult result = client().copyObject(cloud.s3Bucket(), source.getPath(), cloud.s3Bucket(), target.getPath());
|
||||
client().deleteObject(cloud.s3Bucket(), source.getPath());
|
||||
return S3CloudNodeFactory.file(target.getParent(), target.getName(), ((S3File) source).getSize(), Optional.of(result.getLastModifiedDate()));
|
||||
}
|
||||
}
|
||||
|
||||
public S3File write(S3File file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, 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)));
|
||||
|
||||
final CompletableFuture<Optional<ObjectMetadata>> result = new CompletableFuture<>();
|
||||
|
||||
try {
|
||||
if (size <= CHUNKED_UPLOAD_MAX_SIZE) {
|
||||
uploadFile(file, data, progressAware, result, size);
|
||||
} else {
|
||||
uploadChunkedFile(file, data, progressAware, result, size);
|
||||
}
|
||||
} catch(AmazonS3Exception ex) {
|
||||
handleApiError(ex, file.getName());
|
||||
}
|
||||
|
||||
try {
|
||||
Optional<ObjectMetadata> objectMetadataOptional = result.get();
|
||||
ObjectMetadata objectMetadata = objectMetadataOptional.orElseGet(() -> client().getObjectMetadata(cloud.s3Bucket(), file.getPath()));
|
||||
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
|
||||
return S3CloudNodeFactory.file(file.getParent(), file.getName(), objectMetadata);
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void uploadFile(final S3File file, DataSource data, final ProgressAware<UploadState> progressAware, CompletableFuture<Optional<ObjectMetadata>> result, final long size) //
|
||||
throws IOException {
|
||||
AtomicLong bytesTransferred = new AtomicLong(0);
|
||||
ProgressListener listener = progressEvent -> {
|
||||
bytesTransferred.set(bytesTransferred.get() + progressEvent.getBytesTransferred());
|
||||
progressAware.onProgress( //
|
||||
progress(UploadState.upload(file)) //
|
||||
.between(0) //
|
||||
.and(size) //
|
||||
.withValue(bytesTransferred.get()));
|
||||
};
|
||||
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
metadata.setContentLength(data.size(context).get());
|
||||
|
||||
PutObjectRequest request = new PutObjectRequest(cloud.s3Bucket(), file.getPath(), data.open(context), metadata);
|
||||
request.setGeneralProgressListener(listener);
|
||||
|
||||
result.complete(Optional.of(client().putObject(request).getMetadata()));
|
||||
}
|
||||
|
||||
private void uploadChunkedFile(final S3File file, DataSource data, final ProgressAware<UploadState> progressAware, CompletableFuture<Optional<ObjectMetadata>> result, final long size) //
|
||||
throws IOException {
|
||||
|
||||
TransferUtility tu = TransferUtility //
|
||||
.builder() //
|
||||
.s3Client(client()) //
|
||||
.context(context) //
|
||||
.defaultBucket(cloud.s3Bucket()) //
|
||||
.build();
|
||||
|
||||
TransferListener transferListener = new TransferListener() {
|
||||
@Override
|
||||
public void onStateChanged(int id, TransferState state) {
|
||||
if (state.equals(TransferState.COMPLETED)) {
|
||||
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
|
||||
result.complete(Optional.empty());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) {
|
||||
progressAware.onProgress( //
|
||||
progress(UploadState.upload(file)) //
|
||||
.between(0) //
|
||||
.and(size) //
|
||||
.withValue(bytesCurrent));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int id, Exception ex) {
|
||||
result.fail(ex);
|
||||
}
|
||||
};
|
||||
|
||||
UploadOptions uploadOptions = UploadOptions.builder().transferListener(transferListener).build();
|
||||
|
||||
tu.upload(file.getPath(), data.open(context), uploadOptions);
|
||||
}
|
||||
|
||||
public void read(S3File file, Optional<File> encryptedTmpFile, OutputStream data, final ProgressAware<DownloadState> progressAware) throws IOException, BackendException {
|
||||
progressAware.onProgress(Progress.started(DownloadState.download(file)));
|
||||
|
||||
Optional<String> cacheKey = Optional.empty();
|
||||
Optional<File> cacheFile = Optional.empty();
|
||||
|
||||
ListObjectsV2Result listObjects;
|
||||
|
||||
if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) {
|
||||
listObjects = client().listObjectsV2(cloud.s3Bucket(), file.getKey());
|
||||
if (listObjects.getObjectSummaries().size() != 1) {
|
||||
throw new NoSuchCloudFileException(file.getKey());
|
||||
}
|
||||
S3ObjectSummary summary = listObjects.getObjectSummaries().get(0);
|
||||
cacheKey = Optional.of(summary.getKey() + summary.getETag());
|
||||
|
||||
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("S3Impl").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 S3File file, //
|
||||
final OutputStream data, //
|
||||
final Optional<File> encryptedTmpFile, //
|
||||
final Optional<String> cacheKey, //
|
||||
final ProgressAware<DownloadState> progressAware) throws IOException, BackendException {
|
||||
AtomicLong bytesTransferred = new AtomicLong(0);
|
||||
ProgressListener listener = progressEvent -> {
|
||||
bytesTransferred.set(bytesTransferred.get() + progressEvent.getBytesTransferred());
|
||||
|
||||
progressAware.onProgress( //
|
||||
progress(DownloadState.download(file)) //
|
||||
.between(0) //
|
||||
.and(file.getSize().orElse(Long.MAX_VALUE)) //
|
||||
.withValue(bytesTransferred.get()));
|
||||
};
|
||||
|
||||
GetObjectRequest request = new GetObjectRequest(cloud.s3Bucket(), file.getPath());
|
||||
request.setGeneralProgressListener(listener);
|
||||
|
||||
try {
|
||||
S3Object s3Object = client().getObject(request);
|
||||
|
||||
CopyStream.copyStreamToStream(s3Object.getObjectContent(), data);
|
||||
|
||||
if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) {
|
||||
try {
|
||||
storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get());
|
||||
} catch (IOException e) {
|
||||
Timber.tag("S3Impl").e(e, "Failed to write downloaded file in LRU cache");
|
||||
}
|
||||
}
|
||||
} catch(AmazonS3Exception ex) {
|
||||
handleApiError(ex, file.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void delete(S3Node node) throws IOException, BackendException {
|
||||
if (node instanceof S3Folder) {
|
||||
List<S3ObjectSummary> summaries = client().listObjectsV2(cloud.s3Bucket(), node.getPath()).getObjectSummaries();
|
||||
|
||||
List<KeyVersion> keys = new ArrayList<>();
|
||||
for (S3ObjectSummary summary : summaries) {
|
||||
keys.add(new KeyVersion(summary.getKey()));
|
||||
}
|
||||
|
||||
DeleteObjectsRequest request = new DeleteObjectsRequest(cloud.s3Bucket());
|
||||
request.withKeys(keys);
|
||||
|
||||
client().deleteObjects(request);
|
||||
} else {
|
||||
client().deleteObject(cloud.s3Bucket(), node.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
public String checkAuthenticationAndRetrieveCurrentAccount() throws NoSuchBucketException {
|
||||
if (!client().doesBucketExist(cloud.s3Bucket())) {
|
||||
throw new NoSuchBucketException(cloud.s3Bucket());
|
||||
}
|
||||
|
||||
Owner currentAccount = client() //
|
||||
.getS3AccountOwner();
|
||||
|
||||
return currentAccount.getDisplayName();
|
||||
}
|
||||
|
||||
private boolean createLruCache(int cacheSize) {
|
||||
if (diskLruCache == null) {
|
||||
try {
|
||||
diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(S3), cacheSize);
|
||||
} catch (IOException e) {
|
||||
Timber.tag("S3Impl").e(e, "Failed to setup LRU cache");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleApiError(AmazonS3Exception ex, String name) throws BackendException {
|
||||
String errorCode = ex.getErrorCode();
|
||||
if (S3CloudApiExceptions.isAccessProblem(errorCode)) {
|
||||
throw new ForbiddenException();
|
||||
} else if (S3CloudApiErrorCodes.NO_SUCH_BUCKET.getValue().equals(errorCode)) {
|
||||
throw new NoSuchBucketException(name);
|
||||
} else if (S3CloudApiErrorCodes.NO_SUCH_KEY.getValue().equals(errorCode)) {
|
||||
throw new NoSuchCloudFileException(name);
|
||||
} else {
|
||||
throw new FatalBackendException(ex);
|
||||
}
|
||||
}
|
||||
}
|
12
data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.java
Normal file
@ -0,0 +1,12 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import org.cryptomator.domain.CloudNode;
|
||||
|
||||
interface S3Node extends CloudNode {
|
||||
|
||||
@Override
|
||||
S3Folder getParent();
|
||||
|
||||
String getKey();
|
||||
|
||||
}
|
@ -23,14 +23,16 @@ class DatabaseUpgrades {
|
||||
Upgrade1To2 upgrade1To2, //
|
||||
Upgrade2To3 upgrade2To3, //
|
||||
Upgrade3To4 upgrade3To4, //
|
||||
Upgrade4To5 upgrade4To5) {
|
||||
Upgrade4To5 upgrade4To5, //
|
||||
Upgrade5To6 upgrade5To6) {
|
||||
|
||||
availableUpgrades = defineUpgrades( //
|
||||
upgrade0To1, //
|
||||
upgrade1To2, //
|
||||
upgrade2To3, //
|
||||
upgrade3To4, //
|
||||
upgrade4To5);
|
||||
upgrade4To5, //
|
||||
upgrade5To6);
|
||||
}
|
||||
|
||||
private static Comparator<DatabaseUpgrade> reverseOrder() {
|
||||
|
76
data/src/main/java/org/cryptomator/data/db/Upgrade5To6.kt
Normal file
@ -0,0 +1,76 @@
|
||||
package org.cryptomator.data.db
|
||||
|
||||
import org.greenrobot.greendao.database.Database
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class Upgrade5To6 @Inject constructor() : DatabaseUpgrade(5, 6) {
|
||||
|
||||
override fun internalApplyTo(db: Database, origin: Int) {
|
||||
db.beginTransaction()
|
||||
try {
|
||||
changeCloudEntityToSupportS3(db)
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeCloudEntityToSupportS3(db: Database) {
|
||||
Sql.alterTable("CLOUD_ENTITY").renameTo("CLOUD_ENTITY_OLD").executeOn(db)
|
||||
|
||||
Sql.createTable("CLOUD_ENTITY") //
|
||||
.id() //
|
||||
.requiredText("TYPE") //
|
||||
.optionalText("ACCESS_TOKEN") //
|
||||
.optionalText("URL") //
|
||||
.optionalText("USERNAME") //
|
||||
.optionalText("WEBDAV_CERTIFICATE") //
|
||||
.optionalText("S3_BUCKET") //
|
||||
.optionalText("S3_REGION") //
|
||||
.optionalText("S3_SECRET_KEY") //
|
||||
.executeOn(db);
|
||||
|
||||
Sql.insertInto("CLOUD_ENTITY") //
|
||||
.select("_id", "TYPE", "ACCESS_TOKEN", "URL", "USERNAME", "WEBDAV_CERTIFICATE") //
|
||||
.columns("_id", "TYPE", "ACCESS_TOKEN", "URL", "USERNAME", "WEBDAV_CERTIFICATE") //
|
||||
.from("CLOUD_ENTITY_OLD") //
|
||||
.executeOn(db)
|
||||
|
||||
recreateVaultEntity(db)
|
||||
|
||||
Sql.dropTable("CLOUD_ENTITY_OLD").executeOn(db)
|
||||
}
|
||||
|
||||
private fun recreateVaultEntity(db: Database) {
|
||||
Sql.alterTable("VAULT_ENTITY").renameTo("VAULT_ENTITY_OLD").executeOn(db)
|
||||
Sql.createTable("VAULT_ENTITY") //
|
||||
.id() //
|
||||
.optionalInt("FOLDER_CLOUD_ID") //
|
||||
.optionalText("FOLDER_PATH") //
|
||||
.optionalText("FOLDER_NAME") //
|
||||
.requiredText("CLOUD_TYPE") //
|
||||
.optionalText("PASSWORD") //
|
||||
.optionalInt("POSITION") //
|
||||
.foreignKey("FOLDER_CLOUD_ID", "CLOUD_ENTITY", Sql.SqlCreateTableBuilder.ForeignKeyBehaviour.ON_DELETE_SET_NULL) //
|
||||
.executeOn(db)
|
||||
|
||||
Sql.insertInto("VAULT_ENTITY") //
|
||||
.select("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "POSITION", "CLOUD_ENTITY.TYPE") //
|
||||
.columns("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "POSITION", "CLOUD_TYPE") //
|
||||
.from("VAULT_ENTITY_OLD") //
|
||||
.join("CLOUD_ENTITY", "VAULT_ENTITY_OLD.FOLDER_CLOUD_ID") //
|
||||
.executeOn(db)
|
||||
|
||||
Sql.dropIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID").executeOn(db)
|
||||
|
||||
Sql.createUniqueIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID") //
|
||||
.on("VAULT_ENTITY") //
|
||||
.asc("FOLDER_PATH") //
|
||||
.asc("FOLDER_CLOUD_ID") //
|
||||
.executeOn(db)
|
||||
|
||||
Sql.dropTable("VAULT_ENTITY_OLD").executeOn(db)
|
||||
}
|
||||
}
|
@ -22,14 +22,23 @@ public class CloudEntity extends DatabaseEntity {
|
||||
|
||||
private String webdavCertificate;
|
||||
|
||||
@Generated(hash = 361171073)
|
||||
public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate) {
|
||||
private String s3Bucket;
|
||||
|
||||
private String s3Region;
|
||||
|
||||
private String s3SecretKey;
|
||||
|
||||
@Generated(hash = 1685351705)
|
||||
public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate, String s3Bucket, String s3Region, String s3SecretKey) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.accessToken = accessToken;
|
||||
this.url = url;
|
||||
this.username = username;
|
||||
this.webdavCertificate = webdavCertificate;
|
||||
this.s3Bucket = s3Bucket;
|
||||
this.s3Region = s3Region;
|
||||
this.s3SecretKey = s3SecretKey;
|
||||
}
|
||||
|
||||
@Generated(hash = 1354152224)
|
||||
@ -83,4 +92,28 @@ public class CloudEntity extends DatabaseEntity {
|
||||
public void setWebdavCertificate(String webdavCertificate) {
|
||||
this.webdavCertificate = webdavCertificate;
|
||||
}
|
||||
|
||||
public String getS3Bucket() {
|
||||
return this.s3Bucket;
|
||||
}
|
||||
|
||||
public void setS3Bucket(String s3Bucket) {
|
||||
this.s3Bucket = s3Bucket;
|
||||
}
|
||||
|
||||
public String getS3Region() {
|
||||
return this.s3Region;
|
||||
}
|
||||
|
||||
public void setS3Region(String s3Region) {
|
||||
this.s3Region = s3Region;
|
||||
}
|
||||
|
||||
public String getS3SecretKey() {
|
||||
return this.s3SecretKey;
|
||||
}
|
||||
|
||||
public void setS3SecretKey(String s3SecretKey) {
|
||||
this.s3SecretKey = s3SecretKey;
|
||||
}
|
||||
}
|
||||
|
@ -182,9 +182,7 @@ public class VaultEntity extends DatabaseEntity {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
/**
|
||||
* called by internal mechanisms, do not call yourself.
|
||||
*/
|
||||
/** called by internal mechanisms, do not call yourself. */
|
||||
@Generated(hash = 674742652)
|
||||
public void __setDaoSession(DaoSession daoSession) {
|
||||
this.daoSession = daoSession;
|
||||
|
@ -8,6 +8,7 @@ import org.cryptomator.domain.GoogleDriveCloud;
|
||||
import org.cryptomator.domain.LocalStorageCloud;
|
||||
import org.cryptomator.domain.OnedriveCloud;
|
||||
import org.cryptomator.domain.PCloud;
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
import org.cryptomator.domain.WebDavCloud;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@ -18,6 +19,7 @@ import static org.cryptomator.domain.GoogleDriveCloud.aGoogleDriveCloud;
|
||||
import static org.cryptomator.domain.LocalStorageCloud.aLocalStorage;
|
||||
import static org.cryptomator.domain.OnedriveCloud.aOnedriveCloud;
|
||||
import static org.cryptomator.domain.PCloud.aPCloud;
|
||||
import static org.cryptomator.domain.S3Cloud.aS3Cloud;
|
||||
import static org.cryptomator.domain.WebDavCloud.aWebDavCloudCloud;
|
||||
|
||||
@Singleton
|
||||
@ -43,6 +45,10 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
|
||||
.withAccessToken(entity.getAccessToken()) //
|
||||
.withUsername(entity.getUsername()) //
|
||||
.build();
|
||||
case LOCAL:
|
||||
return aLocalStorage() //
|
||||
.withId(entity.getId()) //
|
||||
.withRootUri(entity.getAccessToken()).build();
|
||||
case ONEDRIVE:
|
||||
return aOnedriveCloud() //
|
||||
.withId(entity.getId()) //
|
||||
@ -56,10 +62,16 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
|
||||
.withAccessToken(entity.getAccessToken()) //
|
||||
.withUsername(entity.getUsername()) //
|
||||
.build();
|
||||
case LOCAL:
|
||||
return aLocalStorage() //
|
||||
case S3:
|
||||
return aS3Cloud() //
|
||||
.withId(entity.getId()) //
|
||||
.withRootUri(entity.getAccessToken()).build();
|
||||
.withS3Endpoint(entity.getUrl()) //
|
||||
.withS3Region(entity.getS3Region()) //
|
||||
.withAccessKey(entity.getAccessToken()) //
|
||||
.withSecretKey(entity.getS3SecretKey()) //
|
||||
.withS3Bucket(entity.getS3Bucket()) //
|
||||
.withDisplayName(entity.getUsername()) //
|
||||
.build();
|
||||
case WEBDAV:
|
||||
return aWebDavCloudCloud() //
|
||||
.withId(entity.getId()) //
|
||||
@ -87,6 +99,9 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
|
||||
result.setAccessToken(((GoogleDriveCloud) domainObject).accessToken());
|
||||
result.setUsername(((GoogleDriveCloud) domainObject).username());
|
||||
break;
|
||||
case LOCAL:
|
||||
result.setAccessToken(((LocalStorageCloud) domainObject).rootUri());
|
||||
break;
|
||||
case ONEDRIVE:
|
||||
result.setAccessToken(((OnedriveCloud) domainObject).accessToken());
|
||||
result.setUsername(((OnedriveCloud) domainObject).username());
|
||||
@ -96,8 +111,13 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
|
||||
result.setUrl(((PCloud) domainObject).url());
|
||||
result.setUsername(((PCloud) domainObject).username());
|
||||
break;
|
||||
case LOCAL:
|
||||
result.setAccessToken(((LocalStorageCloud) domainObject).rootUri());
|
||||
case S3:
|
||||
result.setUrl(((S3Cloud) domainObject).s3Endpoint());
|
||||
result.setS3Region(((S3Cloud) domainObject).s3Region());
|
||||
result.setAccessToken(((S3Cloud) domainObject).accessKey());
|
||||
result.setS3SecretKey(((S3Cloud) domainObject).secretKey());
|
||||
result.setS3Bucket(((S3Cloud) domainObject).s3Bucket());
|
||||
result.setUsername(((S3Cloud) domainObject).displayName());
|
||||
break;
|
||||
case WEBDAV:
|
||||
result.setAccessToken(((WebDavCloud) domainObject).password());
|
||||
|
@ -6,6 +6,7 @@ import org.cryptomator.data.cloud.googledrive.GoogleDriveCloudContentRepositoryF
|
||||
import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.pcloud.PCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -27,6 +28,7 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
|
||||
GoogleDriveCloudContentRepositoryFactory googleDriveFactory, //
|
||||
OnedriveCloudContentRepositoryFactory oneDriveFactory, //
|
||||
PCloudContentRepositoryFactory pCloudFactory, //
|
||||
S3CloudContentRepositoryFactory s3Factory, //
|
||||
CryptoCloudContentRepositoryFactory cryptoFactory, //
|
||||
LocalStorageContentRepositoryFactory localStorageFactory, //
|
||||
WebDavCloudContentRepositoryFactory webDavFactory) {
|
||||
@ -35,6 +37,7 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
|
||||
googleDriveFactory, //
|
||||
oneDriveFactory, //
|
||||
pCloudFactory, //
|
||||
s3Factory, //
|
||||
cryptoFactory, //
|
||||
localStorageFactory, //
|
||||
webDavFactory);
|
||||
|
@ -2,6 +2,6 @@ package org.cryptomator.domain;
|
||||
|
||||
public enum CloudType {
|
||||
|
||||
DROPBOX, GOOGLE_DRIVE, ONEDRIVE, PCLOUD, WEBDAV, LOCAL, CRYPTO
|
||||
DROPBOX, GOOGLE_DRIVE, ONEDRIVE, PCLOUD, WEBDAV, LOCAL, S3, CRYPTO
|
||||
|
||||
}
|
||||
|
179
domain/src/main/java/org/cryptomator/domain/S3Cloud.java
Normal file
@ -0,0 +1,179 @@
|
||||
package org.cryptomator.domain;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class S3Cloud implements Cloud {
|
||||
|
||||
private final Long id;
|
||||
private final String accessKey;
|
||||
private final String secretKey;
|
||||
private final String s3Bucket;
|
||||
private final String s3Endpoint;
|
||||
private final String s3Region;
|
||||
private final String displayName;
|
||||
|
||||
private S3Cloud(Builder builder) {
|
||||
this.id = builder.id;
|
||||
this.accessKey = builder.accessKey;
|
||||
this.secretKey = builder.secretKey;
|
||||
this.s3Bucket = builder.s3Bucket;
|
||||
this.s3Endpoint = builder.s3Endpoint;
|
||||
this.s3Region = builder.s3Region;
|
||||
this.displayName = builder.displayName;
|
||||
}
|
||||
|
||||
public static Builder aS3Cloud() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static Builder aCopyOf(S3Cloud s3Cloud) {
|
||||
return new Builder() //
|
||||
.withId(s3Cloud.id()) //
|
||||
.withAccessKey(s3Cloud.accessKey()) //
|
||||
.withSecretKey(s3Cloud.secretKey()) //
|
||||
.withS3Bucket(s3Cloud.s3Bucket()) //
|
||||
.withS3Endpoint(s3Cloud.s3Endpoint()) //
|
||||
.withS3Region(s3Cloud.s3Region()) //
|
||||
.withDisplayName(s3Cloud.displayName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String accessKey() {
|
||||
return accessKey;
|
||||
}
|
||||
|
||||
public String secretKey() {
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
public String s3Bucket() {
|
||||
return s3Bucket;
|
||||
}
|
||||
|
||||
public String s3Endpoint() {
|
||||
return s3Endpoint;
|
||||
}
|
||||
|
||||
public String s3Region() {
|
||||
return s3Region;
|
||||
}
|
||||
|
||||
public String displayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloudType type() {
|
||||
return CloudType.S3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configurationMatches(Cloud cloud) {
|
||||
return cloud instanceof S3Cloud && configurationMatches((S3Cloud) cloud);
|
||||
}
|
||||
|
||||
private boolean configurationMatches(S3Cloud cloud) {
|
||||
return s3Bucket.equals(cloud.s3Bucket) && s3Endpoint.equals(cloud.s3Endpoint) && s3Region.equals(cloud.s3Region);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean predefined() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persistent() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresNetwork() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "S3";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
return internalEquals((S3Cloud) obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id == null ? 0 : id.hashCode();
|
||||
}
|
||||
|
||||
private boolean internalEquals(S3Cloud obj) {
|
||||
return id != null && id.equals(obj.id);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private Long id;
|
||||
private String accessKey;
|
||||
private String secretKey;
|
||||
private String s3Bucket;
|
||||
private String s3Endpoint;
|
||||
private String s3Region;
|
||||
private String displayName;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder withId(Long id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withAccessKey(String accessKey) {
|
||||
this.accessKey = accessKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withSecretKey(String secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withS3Bucket(String s3Bucket) {
|
||||
this.s3Bucket = s3Bucket;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withS3Endpoint(String s3Endpoint) {
|
||||
this.s3Endpoint = s3Endpoint;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withS3Region(String s3Region) {
|
||||
this.s3Region = s3Region;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public S3Cloud build() {
|
||||
return new S3Cloud(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package org.cryptomator.domain.exception;
|
||||
|
||||
public class NoSuchBucketException extends BackendException {
|
||||
|
||||
public NoSuchBucketException() {
|
||||
}
|
||||
|
||||
public NoSuchBucketException(String name) {
|
||||
super(name);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package org.cryptomator.domain.usecases.cloud;
|
||||
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
import org.cryptomator.domain.exception.BackendException;
|
||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||
import org.cryptomator.generator.Parameter;
|
||||
import org.cryptomator.generator.UseCase;
|
||||
|
||||
@UseCase
|
||||
class ConnectToS3 {
|
||||
|
||||
private final CloudContentRepository cloudContentRepository;
|
||||
private final S3Cloud cloud;
|
||||
|
||||
public ConnectToS3(CloudContentRepository cloudContentRepository, @Parameter S3Cloud cloud) {
|
||||
this.cloudContentRepository = cloudContentRepository;
|
||||
this.cloud = cloud;
|
||||
}
|
||||
|
||||
public void execute() throws BackendException {
|
||||
cloudContentRepository.checkAuthenticationAndRetrieveCurrentAccount(cloud);
|
||||
}
|
||||
}
|
@ -177,25 +177,31 @@ platform :android do |options|
|
||||
}
|
||||
)
|
||||
|
||||
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "repo/Cryptomator.apk")
|
||||
if options[:beta]
|
||||
puts "Skipping deployment to F-Droid cause there isn't currently a beta channel"
|
||||
else
|
||||
puts "Updating F-Droid"
|
||||
|
||||
sh("cp -r metadata/android/ metadata/org.cryptomator/")
|
||||
FileUtils.cp("metadata/org.cryptomator/en-US/changelogs/default.txt", "metadata/org.cryptomator/en-US/changelogs/#{version}.txt")
|
||||
FileUtils.cp("metadata/org.cryptomator/de-DE/changelogs/default.txt", "metadata/org.cryptomator/de-DE/changelogs/#{version}.txt")
|
||||
sh("fdroid update && fdroid rewritemeta")
|
||||
sh("rm -r metadata/org.cryptomator/")
|
||||
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "repo/Cryptomator.apk")
|
||||
|
||||
aws_s3(
|
||||
bucket: ENV['S3_BUCKET'],
|
||||
endpoint: ENV['S3_ENDPOINT'],
|
||||
region: ENV['S3_REGION'],
|
||||
access_key: ENV['S3_ACCESS_KEY'],
|
||||
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
|
||||
path: "android/fdroid",
|
||||
folder: "fastlane/repo",
|
||||
skip_html_upload: true,
|
||||
apk: ''
|
||||
)
|
||||
sh("cp -r metadata/android/ metadata/org.cryptomator/")
|
||||
FileUtils.cp("metadata/org.cryptomator/en-US/changelogs/default.txt", "metadata/org.cryptomator/en-US/changelogs/#{version}.txt")
|
||||
FileUtils.cp("metadata/org.cryptomator/de-DE/changelogs/default.txt", "metadata/org.cryptomator/de-DE/changelogs/#{version}.txt")
|
||||
sh("fdroid update && fdroid rewritemeta")
|
||||
sh("rm -r metadata/org.cryptomator/")
|
||||
|
||||
aws_s3(
|
||||
bucket: ENV['S3_BUCKET'],
|
||||
endpoint: ENV['S3_ENDPOINT'],
|
||||
region: ENV['S3_REGION'],
|
||||
access_key: ENV['S3_ACCESS_KEY'],
|
||||
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
|
||||
path: "android/fdroid",
|
||||
folder: "fastlane/repo",
|
||||
skip_html_upload: true,
|
||||
apk: ''
|
||||
)
|
||||
end
|
||||
|
||||
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}_fdroid_signed.apk")
|
||||
end
|
||||
|
@ -1 +1,3 @@
|
||||
- Problem bei der pCloud-Anmeldung in der F-Droid-Variante behoben
|
||||
- Unterstützung für S3-kompatiblen Speicher hinzugefügt
|
||||
- Passwortrichtlinie verbessert, so dass Tresore mit sehr schlechten Passwörtern nicht mehr erstellt werden können
|
||||
- Problem bei der Dateisuche behoben, falls Globbing aktiviert, die Live-Suche deaktiviert und das Muster Großbuchstaben enthält
|
@ -1 +1,3 @@
|
||||
- Fixed pCloud login using F-Droid
|
||||
- Added support for S3 compatible storage
|
||||
- Enhanced password policy so that vaults with very bad passwords can no longer be created
|
||||
- Fixed problem in file search when globbing is enabled, live search is disabled and pattern contains uppercase characters
|
@ -1,3 +1,5 @@
|
||||
<ul>
|
||||
<li>Fixed pCloud login using F-Droid</li>
|
||||
<li>Added support for S3 compatible storage</li>
|
||||
<li>Enhanced password policy so that vaults with very bad passwords can no longer be created</li>
|
||||
<li>Fixed problem in file search when globbing is enabled, live search is disabled and pattern contains uppercase characters</li>
|
||||
</ul>
|
@ -37,10 +37,12 @@ import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||
import org.cryptomator.presentation.exception.PermissionNotGrantedException
|
||||
import org.cryptomator.presentation.intent.AuthenticateCloudIntent
|
||||
import org.cryptomator.presentation.intent.Intents
|
||||
import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.ProgressModel
|
||||
import org.cryptomator.presentation.model.ProgressStateModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import org.cryptomator.presentation.model.mappers.CloudModelMapper
|
||||
import org.cryptomator.presentation.ui.activity.view.AuthenticateCloudView
|
||||
@ -72,6 +74,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
OnedriveAuthStrategy(), //
|
||||
PCloudAuthStrategy(), //
|
||||
WebDAVAuthStrategy(), //
|
||||
S3AuthStrategy(), //
|
||||
LocalStorageAuthStrategy() //
|
||||
)
|
||||
|
||||
@ -404,6 +407,38 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
finish()
|
||||
}
|
||||
|
||||
private inner class S3AuthStrategy : AuthStrategy {
|
||||
|
||||
private var authenticationStarted = false
|
||||
|
||||
override fun supports(cloud: CloudModel): Boolean {
|
||||
return cloud.cloudType() == CloudTypeModel.S3
|
||||
}
|
||||
|
||||
override fun resumed(intent: AuthenticateCloudIntent) {
|
||||
when {
|
||||
ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> {
|
||||
if (!authenticationStarted) {
|
||||
startAuthentication(intent.cloud())
|
||||
Toast.makeText(
|
||||
context(),
|
||||
String.format(getString(R.string.error_authentication_failed), intent.cloud().username()),
|
||||
Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.tag("AuthicateCloudPrester").e(intent.error())
|
||||
failAuthentication(intent.cloud().name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAuthentication(cloud: CloudModel) {
|
||||
authenticationStarted = true
|
||||
startIntent(Intents.s3AddOrChangeIntent().withS3Cloud(cloud as S3CloudModel))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LocalStorageAuthStrategy : AuthStrategy {
|
||||
|
||||
private var authenticationStarted = false
|
||||
|
@ -108,6 +108,7 @@
|
||||
<activity android:name=".ui.activity.LicensesActivity" />
|
||||
<activity android:name=".ui.activity.SettingsActivity" />
|
||||
<activity android:name=".ui.activity.WebDavAddOrChangeActivity" />
|
||||
<activity android:name=".ui.activity.S3AddOrChangeActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.AuthenticateCloudActivity"
|
||||
|
@ -16,6 +16,7 @@ import org.cryptomator.presentation.ui.activity.EmptyDirIdFileInfoActivity;
|
||||
import org.cryptomator.presentation.ui.activity.ImagePreviewActivity;
|
||||
import org.cryptomator.presentation.ui.activity.LicenseCheckActivity;
|
||||
import org.cryptomator.presentation.ui.activity.LicensesActivity;
|
||||
import org.cryptomator.presentation.ui.activity.S3AddOrChangeActivity;
|
||||
import org.cryptomator.presentation.ui.activity.SetPasswordActivity;
|
||||
import org.cryptomator.presentation.ui.activity.SettingsActivity;
|
||||
import org.cryptomator.presentation.ui.activity.SharedFilesActivity;
|
||||
@ -31,6 +32,7 @@ import org.cryptomator.presentation.ui.fragment.CloudConnectionListFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.CloudSettingsFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.EmptyDirIdFileInfoFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.ImagePreviewFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.S3AddOrChangeFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.SetPasswordFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.SharedFilesFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.TextEditorFragment;
|
||||
@ -114,4 +116,8 @@ public interface ActivityComponent {
|
||||
void inject(AutoUploadChooseVaultFragment autoUploadChooseVaultFragment);
|
||||
|
||||
void inject(LicenseCheckActivity licenseCheckActivity);
|
||||
|
||||
void inject(S3AddOrChangeActivity s3AddOrChangeActivity);
|
||||
|
||||
void inject(S3AddOrChangeFragment s3AddOrChangeFragment);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import org.cryptomator.domain.di.PerView
|
||||
import org.cryptomator.domain.exception.CloudAlreadyExistsException
|
||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
|
||||
import org.cryptomator.domain.exception.NetworkConnectionException
|
||||
import org.cryptomator.domain.exception.NoSuchBucketException
|
||||
import org.cryptomator.domain.exception.NoSuchCloudFileException
|
||||
import org.cryptomator.domain.exception.UnableToDecryptWebdavPasswordException
|
||||
import org.cryptomator.domain.exception.VaultAlreadyExistException
|
||||
@ -45,6 +46,7 @@ class ExceptionHandlers @Inject constructor(private val context: Context, defaul
|
||||
staticHandler(NoLicenseAvailableException::class.java, R.string.dialog_enter_license_no_content)
|
||||
staticHandler(GeneralUpdateErrorException::class.java, R.string.error_general_update)
|
||||
staticHandler(SSLHandshakePreAndroid5UpdateCheckException::class.java, R.string.error_general_update)
|
||||
staticHandler(NoSuchBucketException::class.java, R.string.error_no_such_bucket)
|
||||
exceptionHandlers.add(MissingCryptorExceptionHandler())
|
||||
exceptionHandlers.add(CancellationExceptionHandler())
|
||||
exceptionHandlers.add(NoSuchVaultExceptionHandler())
|
||||
|
@ -0,0 +1,13 @@
|
||||
package org.cryptomator.presentation.intent;
|
||||
|
||||
import org.cryptomator.generator.Intent;
|
||||
import org.cryptomator.generator.Optional;
|
||||
import org.cryptomator.presentation.model.S3CloudModel;
|
||||
import org.cryptomator.presentation.ui.activity.S3AddOrChangeActivity;
|
||||
|
||||
@Intent(S3AddOrChangeActivity.class)
|
||||
public interface S3AddOrChangeIntent {
|
||||
|
||||
@Optional
|
||||
S3CloudModel s3Cloud();
|
||||
}
|
@ -28,6 +28,11 @@ enum class CloudTypeModel(builder: Builder) {
|
||||
.withVaultImageResource(R.drawable.webdav_vault) //
|
||||
.withVaultSelectedImageResource(R.drawable.webdav_vault_selected) //
|
||||
.withMultiInstances()), //
|
||||
S3(Builder("S3", R.string.cloud_names_s3) //
|
||||
.withCloudImageResource(R.drawable.s3) //
|
||||
.withVaultImageResource(R.drawable.s3_vault) //
|
||||
.withVaultSelectedImageResource(R.drawable.s3_vault_selected) //
|
||||
.withMultiInstances()), //
|
||||
LOCAL(Builder("LOCAL", R.string.cloud_names_local_storage) //
|
||||
.withCloudImageResource(R.drawable.local_fs) //
|
||||
.withVaultImageResource(R.drawable.local_fs_vault) //
|
||||
|
@ -0,0 +1,48 @@
|
||||
package org.cryptomator.presentation.model
|
||||
|
||||
import org.cryptomator.domain.Cloud
|
||||
import org.cryptomator.domain.S3Cloud
|
||||
import org.cryptomator.presentation.R
|
||||
|
||||
class S3CloudModel(cloud: Cloud) : CloudModel(cloud) {
|
||||
|
||||
override fun name(): Int {
|
||||
return R.string.cloud_names_s3
|
||||
}
|
||||
|
||||
override fun username(): String {
|
||||
return cloud().displayName()
|
||||
}
|
||||
|
||||
override fun cloudType(): CloudTypeModel {
|
||||
return CloudTypeModel.S3
|
||||
}
|
||||
|
||||
fun id(): Long {
|
||||
return cloud().id()
|
||||
}
|
||||
|
||||
fun accessKey(): String {
|
||||
return cloud().accessKey()
|
||||
}
|
||||
|
||||
fun secretKey(): String {
|
||||
return cloud().secretKey()
|
||||
}
|
||||
|
||||
fun s3Bucket(): String {
|
||||
return cloud().s3Bucket()
|
||||
}
|
||||
|
||||
fun s3Endpoint(): String {
|
||||
return cloud().s3Endpoint()
|
||||
}
|
||||
|
||||
fun s3Region(): String {
|
||||
return cloud().s3Region()
|
||||
}
|
||||
|
||||
private fun cloud(): S3Cloud {
|
||||
return toCloud() as S3Cloud
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import org.cryptomator.presentation.model.GoogleDriveCloudModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.OnedriveCloudModel
|
||||
import org.cryptomator.presentation.model.PCloudModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -24,10 +25,11 @@ class CloudModelMapper @Inject constructor() : ModelMapper<CloudModel, Cloud>()
|
||||
return when (CloudTypeModel.valueOf(domainObject.type())) {
|
||||
CloudTypeModel.DROPBOX -> DropboxCloudModel(domainObject)
|
||||
CloudTypeModel.GOOGLE_DRIVE -> GoogleDriveCloudModel(domainObject)
|
||||
CloudTypeModel.LOCAL -> LocalStorageModel(domainObject)
|
||||
CloudTypeModel.ONEDRIVE -> OnedriveCloudModel(domainObject)
|
||||
CloudTypeModel.PCLOUD -> PCloudModel(domainObject)
|
||||
CloudTypeModel.S3 -> S3CloudModel(domainObject)
|
||||
CloudTypeModel.CRYPTO -> CryptoCloudModel(domainObject)
|
||||
CloudTypeModel.LOCAL -> LocalStorageModel(domainObject)
|
||||
CloudTypeModel.WEBDAV -> WebDavCloudModel(domainObject)
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import org.cryptomator.presentation.intent.Intents
|
||||
import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import org.cryptomator.presentation.model.mappers.CloudModelMapper
|
||||
import org.cryptomator.presentation.ui.activity.view.CloudConnectionListView
|
||||
@ -129,7 +130,7 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
|
||||
fun onAddConnectionClicked() {
|
||||
when (selectedCloudType.get()) {
|
||||
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), //
|
||||
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
|
||||
Intents.webDavAddOrChangeIntent())
|
||||
CloudTypeModel.PCLOUD -> {
|
||||
val authIntent: Intent = AuthorizationActivity.createIntent(
|
||||
@ -143,6 +144,8 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), //
|
||||
authIntent)
|
||||
}
|
||||
CloudTypeModel.S3 -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
|
||||
Intents.s3AddOrChangeIntent())
|
||||
CloudTypeModel.LOCAL -> openDocumentTree()
|
||||
}
|
||||
}
|
||||
@ -165,12 +168,20 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
fun onChangeCloudClicked(cloudModel: CloudModel) {
|
||||
if (cloudModel.cloudType() == CloudTypeModel.WEBDAV) {
|
||||
requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), //
|
||||
Intents.webDavAddOrChangeIntent() //
|
||||
.withWebDavCloud(cloudModel as WebDavCloudModel))
|
||||
} else {
|
||||
throw IllegalStateException("Change cloud with type " + cloudModel.cloudType() + " is not supported")
|
||||
when {
|
||||
cloudModel.cloudType() == CloudTypeModel.WEBDAV -> {
|
||||
requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
|
||||
Intents.webDavAddOrChangeIntent() //
|
||||
.withWebDavCloud(cloudModel as WebDavCloudModel))
|
||||
}
|
||||
cloudModel.cloudType() == CloudTypeModel.S3 -> {
|
||||
requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
|
||||
Intents.s3AddOrChangeIntent() //
|
||||
.withS3Cloud(cloudModel as S3CloudModel))
|
||||
}
|
||||
else -> {
|
||||
throw IllegalStateException("Change cloud with type " + cloudModel.cloudType() + " is not supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +190,7 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
@Callback
|
||||
fun addChangeWebDavCloud(result: ActivityResult?) {
|
||||
fun addChangeMultiCloud(result: ActivityResult?) {
|
||||
loadCloudList()
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package org.cryptomator.presentation.presenter
|
||||
import org.cryptomator.domain.Cloud
|
||||
import org.cryptomator.domain.LocalStorageCloud
|
||||
import org.cryptomator.domain.PCloud
|
||||
import org.cryptomator.domain.S3Cloud
|
||||
import org.cryptomator.domain.WebDavCloud
|
||||
import org.cryptomator.domain.di.PerView
|
||||
import org.cryptomator.domain.exception.FatalBackendException
|
||||
@ -18,6 +19,7 @@ import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.PCloudModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import org.cryptomator.presentation.model.mappers.CloudModelMapper
|
||||
import org.cryptomator.presentation.ui.activity.view.CloudSettingsView
|
||||
@ -37,6 +39,7 @@ class CloudSettingsPresenter @Inject constructor( //
|
||||
CloudTypeModel.CRYPTO, //
|
||||
CloudTypeModel.LOCAL, //
|
||||
CloudTypeModel.PCLOUD, //
|
||||
CloudTypeModel.S3, //
|
||||
CloudTypeModel.WEBDAV)
|
||||
|
||||
fun loadClouds() {
|
||||
@ -44,7 +47,7 @@ class CloudSettingsPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
fun onCloudClicked(cloudModel: CloudModel) {
|
||||
if (isWebdavOrPCloudOrLocal(cloudModel)) {
|
||||
if (cloudModel.cloudType().isMultiInstance) {
|
||||
startConnectionListActivity(cloudModel.cloudType())
|
||||
} else {
|
||||
if (isLoggedIn(cloudModel)) {
|
||||
@ -61,10 +64,6 @@ class CloudSettingsPresenter @Inject constructor( //
|
||||
}
|
||||
}
|
||||
|
||||
private fun isWebdavOrPCloudOrLocal(cloudModel: CloudModel): Boolean {
|
||||
return cloudModel is WebDavCloudModel || cloudModel is LocalStorageModel || cloudModel is PCloudModel
|
||||
}
|
||||
|
||||
private fun loginCloud(cloudModel: CloudModel) {
|
||||
getCloudsUseCase //
|
||||
.withCloudType(CloudTypeModel.valueOf(cloudModel.cloudType())) //
|
||||
@ -93,8 +92,9 @@ class CloudSettingsPresenter @Inject constructor( //
|
||||
|
||||
private fun effectiveTitle(cloudTypeModel: CloudTypeModel): String {
|
||||
when (cloudTypeModel) {
|
||||
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections)
|
||||
CloudTypeModel.PCLOUD -> return context().getString(R.string.screen_cloud_settings_pcloud_connections)
|
||||
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections)
|
||||
CloudTypeModel.S3 -> return context().getString(R.string.screen_cloud_settings_s3_connections)
|
||||
CloudTypeModel.LOCAL -> return context().getString(R.string.screen_cloud_settings_local_storage_locations)
|
||||
}
|
||||
return context().getString(R.string.screen_cloud_settings_title)
|
||||
@ -126,19 +126,24 @@ class CloudSettingsPresenter @Inject constructor( //
|
||||
.filter { cloud -> !(BuildConfig.FLAVOR == "fdroid" && cloud.cloudType() == CloudTypeModel.GOOGLE_DRIVE) } //
|
||||
.toMutableList() //
|
||||
.also {
|
||||
it.add(aWebdavCloud())
|
||||
it.add(aPCloud())
|
||||
it.add(aWebdavCloud())
|
||||
it.add(aS3Cloud())
|
||||
it.add(aLocalCloud())
|
||||
}
|
||||
view?.render(cloudModel)
|
||||
}
|
||||
|
||||
private fun aPCloud(): PCloudModel {
|
||||
return PCloudModel(PCloud.aPCloud().build())
|
||||
}
|
||||
|
||||
private fun aWebdavCloud(): WebDavCloudModel {
|
||||
return WebDavCloudModel(WebDavCloud.aWebDavCloudCloud().build())
|
||||
}
|
||||
|
||||
private fun aPCloud(): PCloudModel {
|
||||
return PCloudModel(PCloud.aPCloud().build())
|
||||
private fun aS3Cloud(): S3CloudModel {
|
||||
return S3CloudModel(S3Cloud.aS3Cloud().build())
|
||||
}
|
||||
|
||||
private fun aLocalCloud(): CloudModel {
|
||||
|
@ -0,0 +1,104 @@
|
||||
package org.cryptomator.presentation.presenter
|
||||
|
||||
import android.widget.Toast
|
||||
import org.cryptomator.domain.Cloud
|
||||
import org.cryptomator.domain.S3Cloud
|
||||
import org.cryptomator.domain.di.PerView
|
||||
import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
|
||||
import org.cryptomator.domain.usecases.cloud.ConnectToS3UseCase
|
||||
import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||
import org.cryptomator.presentation.model.ProgressModel
|
||||
import org.cryptomator.presentation.model.ProgressStateModel
|
||||
import org.cryptomator.presentation.ui.activity.view.S3AddOrChangeView
|
||||
import org.cryptomator.util.crypto.CredentialCryptor
|
||||
import javax.inject.Inject
|
||||
|
||||
@PerView
|
||||
class S3AddOrChangePresenter @Inject internal constructor( //
|
||||
private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
|
||||
private val connectToS3UseCase: ConnectToS3UseCase, //
|
||||
exceptionMappings: ExceptionHandlers) : Presenter<S3AddOrChangeView>(exceptionMappings) {
|
||||
|
||||
fun checkUserInput(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?, displayName: String) {
|
||||
var statusMessage: String? = null
|
||||
|
||||
if (accessKey.isEmpty()) {
|
||||
statusMessage = getString(R.string.screen_s3_settings_msg_access_key_not_empty)
|
||||
}
|
||||
if (secretKey.isEmpty()) {
|
||||
statusMessage = getString(R.string.screen_s3_settings_msg_secret_key_not_empty)
|
||||
}
|
||||
if (bucket.isEmpty()) {
|
||||
statusMessage = getString(R.string.screen_s3_settings_msg_bucket_not_empty)
|
||||
}
|
||||
if (displayName.isEmpty()) {
|
||||
statusMessage = getString(R.string.screen_s3_settings_msg_display_name_not_empty)
|
||||
}
|
||||
if (endpoint.isNullOrEmpty() && region.isNullOrEmpty()) {
|
||||
statusMessage = getString(R.string.screen_s3_settings_msg_endpoint_and_region_not_empty)
|
||||
}
|
||||
|
||||
if (statusMessage != null) {
|
||||
Toast.makeText(context(), statusMessage, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
view?.onCheckUserInputSucceeded(encrypt(accessKey), encrypt(secretKey), bucket, endpoint, region, cloudId, displayName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun encrypt(text: String): String {
|
||||
return CredentialCryptor //
|
||||
.getInstance(context()) //
|
||||
.encrypt(text)
|
||||
}
|
||||
|
||||
private fun mapToCloud(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?, displayName: String): S3Cloud {
|
||||
var builder = S3Cloud //
|
||||
.aS3Cloud() //
|
||||
.withAccessKey(accessKey) //
|
||||
.withSecretKey(secretKey) //
|
||||
.withS3Bucket(bucket) //
|
||||
.withS3Endpoint(endpoint) //
|
||||
.withS3Region(region) //
|
||||
.withDisplayName(displayName)
|
||||
|
||||
cloudId?.let { builder = builder.withId(cloudId) }
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun authenticate(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?, displayName: String) {
|
||||
authenticate(mapToCloud(accessKey, secretKey, bucket, endpoint, region, cloudId, displayName))
|
||||
}
|
||||
|
||||
private fun authenticate(cloud: S3Cloud) {
|
||||
view?.showProgress(ProgressModel(ProgressStateModel.AUTHENTICATION))
|
||||
connectToS3UseCase //
|
||||
.withCloud(cloud) //
|
||||
.run(object : DefaultResultHandler<Void?>() {
|
||||
override fun onSuccess(void: Void?) {
|
||||
onCloudAuthenticated(cloud)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
view?.showProgress(ProgressModel.COMPLETED)
|
||||
super.onError(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun onCloudAuthenticated(cloud: Cloud) {
|
||||
save(cloud)
|
||||
finishWithResult(CloudConnectionListPresenter.SELECTED_CLOUD, cloud)
|
||||
}
|
||||
|
||||
private fun save(cloud: Cloud) {
|
||||
addOrChangeCloudConnectionUseCase //
|
||||
.withCloud(cloud) //
|
||||
.run(DefaultResultHandler())
|
||||
}
|
||||
|
||||
init {
|
||||
unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, connectToS3UseCase)
|
||||
}
|
||||
}
|
@ -46,7 +46,6 @@ import org.cryptomator.presentation.ui.dialog.SymLinkDialog
|
||||
import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog
|
||||
import org.cryptomator.presentation.ui.fragment.BrowseFilesFragment
|
||||
import java.util.ArrayList
|
||||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
import kotlinx.android.synthetic.main.toolbar_layout.toolbar
|
||||
@ -545,7 +544,7 @@ class BrowseFilesActivity : BaseActivity(), //
|
||||
}
|
||||
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
updateFilter(query?.toLowerCase(Locale.getDefault()))
|
||||
updateFilter(query)
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ class CloudConnectionListActivity : BaseActivity(),
|
||||
|
||||
private fun connectionListFragment(): CloudConnectionListFragment = getCurrentFragment(R.id.fragmentContainer) as CloudConnectionListFragment
|
||||
|
||||
override fun createFragment(): Fragment? = CloudConnectionListFragment()
|
||||
override fun createFragment(): Fragment = CloudConnectionListFragment()
|
||||
|
||||
override fun showNodeSettings(cloudModel: CloudModel) {
|
||||
val cloudNodeSettingDialog = //
|
||||
|
@ -0,0 +1,37 @@
|
||||
package org.cryptomator.presentation.ui.activity
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.cryptomator.generator.Activity
|
||||
import org.cryptomator.generator.InjectIntent
|
||||
import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.intent.S3AddOrChangeIntent
|
||||
import org.cryptomator.presentation.presenter.S3AddOrChangePresenter
|
||||
import org.cryptomator.presentation.ui.activity.view.S3AddOrChangeView
|
||||
import org.cryptomator.presentation.ui.fragment.S3AddOrChangeFragment
|
||||
import javax.inject.Inject
|
||||
import kotlinx.android.synthetic.main.toolbar_layout.toolbar
|
||||
|
||||
@Activity
|
||||
class S3AddOrChangeActivity : BaseActivity(), S3AddOrChangeView {
|
||||
|
||||
@Inject
|
||||
lateinit var s3AddOrChangePresenter: S3AddOrChangePresenter
|
||||
|
||||
@InjectIntent
|
||||
lateinit var s3AddOrChangeIntent: S3AddOrChangeIntent
|
||||
|
||||
override fun setupView() {
|
||||
toolbar.setTitle(R.string.screen_s3_settings_title)
|
||||
setSupportActionBar(toolbar)
|
||||
}
|
||||
|
||||
override fun createFragment(): Fragment = S3AddOrChangeFragment.newInstance(s3AddOrChangeIntent.s3Cloud())
|
||||
|
||||
override fun onCheckUserInputSucceeded(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?, displayName: String) {
|
||||
s3AddOrChangeFragment().hideKeyboard()
|
||||
s3AddOrChangePresenter.authenticate(accessKey, secretKey, bucket, endpoint, region, cloudId, displayName)
|
||||
}
|
||||
|
||||
private fun s3AddOrChangeFragment(): S3AddOrChangeFragment = getCurrentFragment(R.id.fragmentContainer) as S3AddOrChangeFragment
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package org.cryptomator.presentation.ui.activity.view
|
||||
|
||||
interface S3AddOrChangeView : View {
|
||||
|
||||
fun onCheckUserInputSucceeded(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?, displayName: String)
|
||||
|
||||
}
|
@ -8,6 +8,7 @@ import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.PCloudModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import org.cryptomator.presentation.model.comparator.CloudModelComparator
|
||||
import org.cryptomator.presentation.ui.adapter.CloudConnectionListAdapter.CloudConnectionHolder
|
||||
@ -53,12 +54,19 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
|
||||
|
||||
itemView.setOnClickListener { callback.onCloudConnectionClicked(cloudModel) }
|
||||
|
||||
if (cloudModel is WebDavCloudModel) {
|
||||
bindWebDavCloudModel(cloudModel)
|
||||
} else if (cloudModel is PCloudModel) {
|
||||
bindPCloudModel(cloudModel)
|
||||
} else if (cloudModel is LocalStorageModel) {
|
||||
bindLocalStorageCloudModel(cloudModel)
|
||||
when (cloudModel) {
|
||||
is WebDavCloudModel -> {
|
||||
bindWebDavCloudModel(cloudModel)
|
||||
}
|
||||
is PCloudModel -> {
|
||||
bindPCloudModel(cloudModel)
|
||||
}
|
||||
is S3CloudModel -> {
|
||||
bindS3loudModel(cloudModel)
|
||||
}
|
||||
is LocalStorageModel -> {
|
||||
bindLocalStorageCloudModel(cloudModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,7 +78,6 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
|
||||
} catch (e: URISyntaxException) {
|
||||
throw FatalBackendException("path in WebDAV cloud isn't correct (no uri)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun bindPCloudModel(cloudModel: PCloudModel) {
|
||||
@ -78,6 +85,12 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
|
||||
itemView.cloudSubText.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
||||
private fun bindS3loudModel(cloudModel: S3CloudModel) {
|
||||
itemView.cloudText.text = cloudModel.username()
|
||||
itemView.cloudSubText.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun bindLocalStorageCloudModel(cloudModel: LocalStorageModel) {
|
||||
if (cloudModel.location().isEmpty()) {
|
||||
itemView.cloudText.text = cloudModel.storage()
|
||||
|
@ -39,19 +39,19 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter<CloudModel,
|
||||
|
||||
itemView.cloudImage.setImageResource(cloudModel.cloudType().cloudImageResource)
|
||||
|
||||
if (webdav(cloudModel.cloudType())) {
|
||||
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections)
|
||||
} else if (pCloud(cloudModel.cloudType())) {
|
||||
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_pcloud_connections)
|
||||
} else if (local(cloudModel.cloudType())) {
|
||||
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_local_storage_locations)
|
||||
} else {
|
||||
itemView.cloudName.text = getCloudNameText(isAlreadyLoggedIn(cloudModel), cloudModel)
|
||||
if (isAlreadyLoggedIn(cloudModel)) {
|
||||
itemView.cloudUsername.text = cloudModel.username()
|
||||
itemView.cloudUsername.visibility = View.VISIBLE
|
||||
} else {
|
||||
itemView.cloudUsername.visibility = View.GONE
|
||||
when (cloudModel.cloudType()) {
|
||||
CloudTypeModel.PCLOUD -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_pcloud_connections)
|
||||
CloudTypeModel.S3 -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_s3_connections)
|
||||
CloudTypeModel.WEBDAV -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections)
|
||||
CloudTypeModel.LOCAL -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_local_storage_locations)
|
||||
else -> {
|
||||
itemView.cloudName.text = getCloudNameText(isAlreadyLoggedIn(cloudModel), cloudModel)
|
||||
if (isAlreadyLoggedIn(cloudModel)) {
|
||||
itemView.cloudUsername.text = cloudModel.username()
|
||||
itemView.cloudUsername.visibility = View.VISIBLE
|
||||
} else {
|
||||
itemView.cloudUsername.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,16 +73,4 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter<CloudModel,
|
||||
context.getString(R.string.screen_cloud_settings_log_in_to)
|
||||
}
|
||||
}
|
||||
|
||||
private fun local(cloudType: CloudTypeModel): Boolean {
|
||||
return CloudTypeModel.LOCAL == cloudType
|
||||
}
|
||||
|
||||
private fun webdav(cloudType: CloudTypeModel): Boolean {
|
||||
return CloudTypeModel.WEBDAV == cloudType
|
||||
}
|
||||
|
||||
private fun pCloud(cloudType: CloudTypeModel): Boolean {
|
||||
return CloudTypeModel.PCLOUD == cloudType
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.PCloudModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import kotlinx.android.synthetic.main.dialog_bottom_sheet_cloud_settings.change_cloud
|
||||
import kotlinx.android.synthetic.main.dialog_bottom_sheet_cloud_settings.delete_cloud
|
||||
@ -30,6 +31,7 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
|
||||
when (cloudModel.cloudType()) {
|
||||
CloudTypeModel.WEBDAV -> bindViewForWebDAV(cloudModel as WebDavCloudModel)
|
||||
CloudTypeModel.PCLOUD -> bindViewForPCloud(cloudModel as PCloudModel)
|
||||
CloudTypeModel.S3 -> bindViewForS3(cloudModel as S3CloudModel)
|
||||
CloudTypeModel.LOCAL -> bindViewForLocal(cloudModel as LocalStorageModel)
|
||||
else -> throw IllegalStateException("Cloud model is not binded in the view")
|
||||
}
|
||||
@ -66,6 +68,11 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
|
||||
tv_cloud_name.text = cloudModel.username()
|
||||
}
|
||||
|
||||
private fun bindViewForS3(cloudModel: S3CloudModel) {
|
||||
change_cloud.visibility = View.VISIBLE
|
||||
tv_cloud_name.text = cloudModel.username()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val CLOUD_NODE_ARG = "cloudModel"
|
||||
|
@ -51,6 +51,14 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
|
||||
changePasswordButton?.let { button ->
|
||||
et_new_retype_password.nextFocusForwardId = button.id
|
||||
}
|
||||
|
||||
registerOnEditorDoneActionAndPerformButtonClick(et_new_retype_password) { changePasswordButton }
|
||||
|
||||
PasswordStrengthUtil() //
|
||||
.startUpdatingPasswordStrengthMeter(et_new_password, //
|
||||
progressBarPwStrengthIndicator, //
|
||||
textViewPwStrengthIndicator, //
|
||||
changePasswordButton)
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,11 +91,6 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
|
||||
|
||||
override fun setupView() {
|
||||
et_old_password.requestFocus()
|
||||
registerOnEditorDoneActionAndPerformButtonClick(et_new_retype_password) { changePasswordButton }
|
||||
PasswordStrengthUtil() //
|
||||
.startUpdatingPasswortStrengthMeter(et_new_password, //
|
||||
progressBarPwStrengthIndicator, //
|
||||
textViewPwStrengthIndicator)
|
||||
dialog?.let { showKeyboard(it) }
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,118 @@
|
||||
package org.cryptomator.presentation.ui.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import org.cryptomator.generator.Fragment
|
||||
import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.presenter.S3AddOrChangePresenter
|
||||
import org.cryptomator.util.crypto.CredentialCryptor
|
||||
import javax.inject.Inject
|
||||
import kotlinx.android.synthetic.main.fragment_setup_s3.accessKeyEditText
|
||||
import kotlinx.android.synthetic.main.fragment_setup_s3.bucketEditText
|
||||
import kotlinx.android.synthetic.main.fragment_setup_s3.createCloudButton
|
||||
import kotlinx.android.synthetic.main.fragment_setup_s3.displayNameEditText
|
||||
import kotlinx.android.synthetic.main.fragment_setup_s3.regionOrEndpointEditText
|
||||
import kotlinx.android.synthetic.main.fragment_setup_s3.regionOrEndpointEditTextLayout
|
||||
import kotlinx.android.synthetic.main.fragment_setup_s3.secretKeyEditText
|
||||
import kotlinx.android.synthetic.main.fragment_setup_s3.toggleCustomS3
|
||||
import timber.log.Timber
|
||||
|
||||
@Fragment(R.layout.fragment_setup_s3)
|
||||
class S3AddOrChangeFragment : BaseFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var s3AddOrChangePresenter: S3AddOrChangePresenter
|
||||
|
||||
private var cloudId: Long? = null
|
||||
|
||||
private val s3CloudModel: S3CloudModel?
|
||||
get() = arguments?.getSerializable(ARG_S3_CLOUD) as? S3CloudModel
|
||||
|
||||
override fun setupView() {
|
||||
createCloudButton.setOnClickListener { createCloud() }
|
||||
createCloudButton.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
createCloud()
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
showEditableCloudContent(s3CloudModel)
|
||||
|
||||
toggleCustomS3.setOnClickListener { switch ->
|
||||
regionOrEndpointEditText.text?.clear()
|
||||
toggleUseAmazonS3((switch as SwitchMaterial).isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleUseAmazonS3(checked: Boolean) = if (checked) {
|
||||
regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_region_label)
|
||||
} else {
|
||||
regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_endpoint_label)
|
||||
}
|
||||
|
||||
private fun showEditableCloudContent(s3CloudModel: S3CloudModel?) {
|
||||
s3CloudModel?.let {
|
||||
cloudId = s3CloudModel.id()
|
||||
displayNameEditText.setText(s3CloudModel.username())
|
||||
accessKeyEditText.setText(decrypt(s3CloudModel.accessKey()))
|
||||
secretKeyEditText.setText(decrypt(s3CloudModel.secretKey()))
|
||||
bucketEditText.setText(s3CloudModel.s3Bucket())
|
||||
|
||||
if (it.s3Endpoint().isNotEmpty()) {
|
||||
toggleCustomS3.isChecked = false
|
||||
regionOrEndpointEditText.setText(s3CloudModel.s3Endpoint())
|
||||
regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_endpoint_label)
|
||||
} else {
|
||||
regionOrEndpointEditText.setText(s3CloudModel.s3Region())
|
||||
regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_region_label)
|
||||
}
|
||||
} ?: regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_region_label)
|
||||
}
|
||||
|
||||
private fun decrypt(text: String?): String {
|
||||
return if (text != null) {
|
||||
try {
|
||||
CredentialCryptor //
|
||||
.getInstance(activity?.applicationContext) //
|
||||
.decrypt(text)
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.tag("S3AddOrChangeFragment").e(e, "Unable to decrypt password, clearing it")
|
||||
""
|
||||
}
|
||||
} else ""
|
||||
}
|
||||
|
||||
private fun createCloud() {
|
||||
val accessKey = accessKeyEditText.text.toString().trim()
|
||||
val secretKey = secretKeyEditText.text.toString().trim()
|
||||
val bucket = bucketEditText.text.toString().trim()
|
||||
val displayName = displayNameEditText.text.toString().trim()
|
||||
|
||||
if (toggleCustomS3.isChecked) {
|
||||
s3AddOrChangePresenter.checkUserInput(accessKey, secretKey, bucket, null, regionOrEndpointEditText.text.toString().trim(), cloudId, displayName)
|
||||
} else {
|
||||
s3AddOrChangePresenter.checkUserInput(accessKey, secretKey, bucket, regionOrEndpointEditText.text.toString().trim(), null, cloudId, displayName)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideKeyboard() {
|
||||
hideKeyboard(bucketEditText)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_S3_CLOUD = "S3_CLOUD"
|
||||
|
||||
fun newInstance(cloudModel: S3CloudModel?): S3AddOrChangeFragment {
|
||||
val result = S3AddOrChangeFragment()
|
||||
val args = Bundle()
|
||||
args.putSerializable(ARG_S3_CLOUD, cloudModel)
|
||||
result.arguments = args
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -29,9 +29,10 @@ class SetPasswordFragment : BaseFragment() {
|
||||
}
|
||||
false
|
||||
}
|
||||
passwordStrengthUtil.startUpdatingPasswortStrengthMeter(passwordEditText, //
|
||||
passwordStrengthUtil.startUpdatingPasswordStrengthMeter(passwordEditText, //
|
||||
progressBarPwStrengthIndicator, //
|
||||
textViewPwStrengthIndicator)
|
||||
textViewPwStrengthIndicator, //
|
||||
createVaultButton)
|
||||
|
||||
passwordEditText.requestFocus()
|
||||
}
|
||||
|
@ -14,13 +14,21 @@ enum class PasswordStrength(val score: Int, val description: Int, val color: Int
|
||||
|
||||
companion object {
|
||||
|
||||
private const val MIN_PASSWORD_LENGTH = 8
|
||||
|
||||
private val zxcvbn = Zxcvbn()
|
||||
|
||||
fun forPassword(password: String, sanitizedInputs: List<String>): PasswordStrength {
|
||||
return if (password.isEmpty()) {
|
||||
EMPTY
|
||||
} else {
|
||||
forScore(zxcvbn.measure(password, sanitizedInputs).score).orElse(EMPTY)
|
||||
return when {
|
||||
password.isEmpty() -> {
|
||||
EMPTY
|
||||
}
|
||||
password.length < MIN_PASSWORD_LENGTH -> {
|
||||
EXTREMELY_WEAK
|
||||
}
|
||||
else -> {
|
||||
forScore(zxcvbn.measure(password, sanitizedInputs).score).orElse(EMPTY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.cryptomator.presentation.util;
|
||||
|
||||
import android.graphics.PorterDuff;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
@ -40,9 +41,10 @@ public class PasswordStrengthUtil {
|
||||
public PasswordStrengthUtil() {
|
||||
}
|
||||
|
||||
public void startUpdatingPasswortStrengthMeter(EditText passwordInput, //
|
||||
public void startUpdatingPasswordStrengthMeter(EditText passwordInput, //
|
||||
final ProgressBar strengthMeter, //
|
||||
final TextView strengthLabel) {
|
||||
final TextView strengthLabel, //
|
||||
final Button button) {
|
||||
RxTextView.textChanges(passwordInput) //
|
||||
.observeOn(Schedulers.computation()) //
|
||||
.map(password -> PasswordStrength.Companion.forPassword(password.toString(), SANITIZED_INPUTS)) //
|
||||
@ -51,6 +53,7 @@ public class PasswordStrengthUtil {
|
||||
strengthMeter.getProgressDrawable().setColorFilter(ResourceHelper.Companion.getColor(strength.getColor()), PorterDuff.Mode.SRC_IN);
|
||||
strengthLabel.setText(strength.getDescription());
|
||||
strengthMeter.setProgress(strength.getScore() + 1);
|
||||
button.setEnabled(strength.getScore() > PasswordStrength.EXTREMELY_WEAK.getScore());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
BIN
presentation/src/main/res/drawable-mdpi/s3.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
presentation/src/main/res/drawable-mdpi/s3_vault.png
Normal file
After Width: | Height: | Size: 888 B |
BIN
presentation/src/main/res/drawable-mdpi/s3_vault_selected.png
Normal file
After Width: | Height: | Size: 551 B |
BIN
presentation/src/main/res/drawable-xhdpi/s3.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
presentation/src/main/res/drawable-xhdpi/s3_vault.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
presentation/src/main/res/drawable-xhdpi/s3_vault_selected.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
presentation/src/main/res/drawable-xxhdpi/s3.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
presentation/src/main/res/drawable-xxhdpi/s3_vault.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
presentation/src/main/res/drawable-xxhdpi/s3_vault_selected.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
114
presentation/src/main/res/layout/fragment_setup_s3.xml
Normal file
@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/activity_vertical_margin">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/displayNameEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/screen_s3_settings_display_name_label"
|
||||
android:imeOptions="flagNoPersonalizedLearning"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/accessKeyEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/screen_s3_settings_access_key_label"
|
||||
android:imeOptions="flagNoPersonalizedLearning"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/secretKeyEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/screen_s3_settings_secret_key_label"
|
||||
android:imeOptions="flagNoPersonalizedLearning"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/bucketEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/screen_s3_settings_bucket_label"
|
||||
android:imeOptions="flagNoPersonalizedLearning"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/regionOrEndpointEditTextLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/regionOrEndpointEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="flagNoPersonalizedLearning"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/toggleCustomS3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:checked="true"
|
||||
android:text="@string/screen_s3_settings_amazon_s3_text" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/createCloudButton"
|
||||
style="?android:textAppearanceSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/screen_webdav_settings_done_button_text"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
@ -27,6 +27,7 @@
|
||||
<string name="error_play_services_not_available">Die Play Services sind nicht installiert</string>
|
||||
<string name="error_biometric_auth_aborted">Biometrischer Login abgebrochen</string>
|
||||
<string name="error_file_not_found_after_opening_using_3party">Lokale Datei ist nach dem Zurückwechseln zu Cryptomator nicht mehr vorhanden. Mögliche Änderungen können nicht in die Cloud übertragen werden.</string>
|
||||
<string name="error_no_such_bucket">Bucket existiert nicht</string>
|
||||
<!-- # clouds -->
|
||||
<!-- ## cloud names -->
|
||||
<string name="cloud_names_local_storage">Lokaler Speicher</string>
|
||||
@ -119,6 +120,12 @@
|
||||
<string name="screen_webdav_settings_msg_url_is_invalid">URL ist ungültig.</string>
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Benutzername muss ausgefüllt werden.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Passwort muss ausgefüllt werden.</string>
|
||||
<!-- ## screen: s3 settings -->
|
||||
<string name="screen_s3_settings_display_name_label">Anzeigename</string>
|
||||
<string name="screen_s3_settings_bucket_label">Vorhandener Bucket</string>
|
||||
<string name="screen_s3_settings_msg_display_name_not_empty">Der Anzeigename darf nicht leer sein</string>
|
||||
<string name="screen_s3_settings_msg_bucket_not_empty">Bucket darf nicht leer sein</string>
|
||||
<string name="screen_s3_settings_msg_endpoint_and_region_not_empty">Endpoint und Region dürfen nicht leer sein</string>
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_msg_name_empty">Tresorname muss ausgefüllt werden.</string>
|
||||
<string name="screen_enter_vault_name_vault_label">Tresorname</string>
|
||||
@ -129,7 +136,7 @@
|
||||
<string name="screen_set_password_button_text">Fertig</string>
|
||||
<string name="screen_set_password_hint">WICHTIG: Wenn Sie Ihr Passwort vergessen, gibt es keine Möglichkeit die Daten zu entschlüsseln.</string>
|
||||
<string name="screen_set_password_retype_password_label">Passwort wiederholen</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Sehr Schwach</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Zu schwach, um einen Tresor zu erstellen</string>
|
||||
<string name="screen_set_password_strength_indicator_1">Schwach</string>
|
||||
<string name="screen_set_password_strength_indicator_2">Mittel</string>
|
||||
<string name="screen_set_password_strength_indicator_3">Stark</string>
|
||||
@ -178,6 +185,7 @@
|
||||
<!-- ## screen: cloud settings -->
|
||||
<string name="screen_cloud_settings_webdav_connections">WebDAV-Verbindungen</string>
|
||||
<string name="screen_cloud_settings_pcloud_connections">pCloud-Verbindungen</string>
|
||||
<string name="screen_cloud_settings_s3_connections">S3-Verbindungen</string>
|
||||
<string name="screen_cloud_settings_local_storage_locations">Lokale Speicherorte</string>
|
||||
<string name="screen_cloud_settings_log_in_to">Einloggen in</string>
|
||||
<string name="screen_cloud_settings_sign_out_from_cloud">Abmelden von</string>
|
||||
|
@ -84,6 +84,7 @@
|
||||
<string name="screen_webdav_settings_msg_url_is_invalid">La URL no es válida.</string>
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">El nombre de usuario no puede estar vacio.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">La contraseña no puede estar vacía.</string>
|
||||
<!-- ## screen: s3 settings -->
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_msg_name_empty">El nombre de la caja fuerte no puede estar vacío.</string>
|
||||
<string name="screen_enter_vault_name_vault_label">Nombre de la caja fuerte</string>
|
||||
|
@ -42,7 +42,7 @@
|
||||
<string name="snack_bar_action_title_sort">Trier par</string>
|
||||
<string name="snack_bar_action_title_sort_az">A - Z</string>
|
||||
<string name="snack_bar_action_title_sort_za">Z - A</string>
|
||||
<string name="snack_bar_action_title_sort_newest">Plus récent</string>
|
||||
<string name="snack_bar_action_title_sort_newest">Récent d\'abord</string>
|
||||
<string name="snack_bar_action_title_sort_oldest">Plus ancien</string>
|
||||
<string name="snack_bar_action_title_sort_biggest">Taille décroissante</string>
|
||||
<string name="snack_bar_action_title_sort_smallest">Taille croissante</string>
|
||||
@ -121,6 +121,7 @@
|
||||
<string name="screen_webdav_settings_msg_url_is_invalid">URL invalide.</string>
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Le nom d\'utilisateur ne peut pas être vide.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Le mot de passe ne peut pas être vide.</string>
|
||||
<!-- ## screen: s3 settings -->
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_msg_name_empty">Le nom du coffre-fort ne peut pas être vide.</string>
|
||||
<string name="screen_enter_vault_name_vault_label">Nom du coffre-fort</string>
|
||||
@ -131,7 +132,6 @@
|
||||
<string name="screen_set_password_button_text">Terminé</string>
|
||||
<string name="screen_set_password_hint">IMPORTANT: Si vous oubliez votre mot de passe, il n\'y aura aucun moyen de récupérer vos données.</string>
|
||||
<string name="screen_set_password_retype_password_label">Retaper le mot de passe</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Très faible</string>
|
||||
<string name="screen_set_password_strength_indicator_1">Faible</string>
|
||||
<string name="screen_set_password_strength_indicator_2">Acceptable</string>
|
||||
<string name="screen_set_password_strength_indicator_3">Fort</string>
|
||||
|
@ -14,6 +14,7 @@
|
||||
<!-- ## screen: choose cloud service -->
|
||||
<!-- ## screen: cloud connections -->
|
||||
<!-- ## screen: webdav settings -->
|
||||
<!-- ## screen: s3 settings -->
|
||||
<!-- ## screen: enter vault name -->
|
||||
<!-- ## screen: set password -->
|
||||
<!-- ## screen: settings -->
|
||||
|
@ -117,6 +117,7 @@
|
||||
<string name="screen_webdav_settings_msg_url_is_invalid">Adres URL jest nieprawidłowy.</string>
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Login nie może być pusty.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Hasło nie może być puste.</string>
|
||||
<!-- ## screen: s3 settings -->
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_msg_name_empty">Nazwa sejfu nie może być pusta.</string>
|
||||
<string name="screen_enter_vault_name_vault_label">Nazwa sejfu</string>
|
||||
@ -127,7 +128,6 @@
|
||||
<string name="screen_set_password_button_text">Gotowe</string>
|
||||
<string name="screen_set_password_hint">WAŻNE: Jeśli zapomnisz hasła, nie ma możliwości odzyskania danych.</string>
|
||||
<string name="screen_set_password_retype_password_label">Wpisz hasło ponownie</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Bardzo słabe</string>
|
||||
<string name="screen_set_password_strength_indicator_1">Słabe</string>
|
||||
<string name="screen_set_password_strength_indicator_2">Średnie</string>
|
||||
<string name="screen_set_password_strength_indicator_3">Mocne</string>
|
||||
|
38
presentation/src/main/res/values-sr/strings.xml
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- # app -->
|
||||
<!-- # error messages -->
|
||||
<!-- # clouds -->
|
||||
<!-- ## cloud names -->
|
||||
<!-- # permission -->
|
||||
<!-- ## permission messages -->
|
||||
<!-- # screens -->
|
||||
<!-- # screen: vault list -->
|
||||
<!-- # screen: file browser -->
|
||||
<!-- ## screen: text editor -->
|
||||
<!-- ## screen: share files -->
|
||||
<!-- ## screen: choose cloud service -->
|
||||
<!-- ## screen: cloud connections -->
|
||||
<!-- ## screen: webdav settings -->
|
||||
<!-- ## screen: s3 settings -->
|
||||
<!-- ## screen: enter vault name -->
|
||||
<!-- ## screen: set password -->
|
||||
<!-- ## screen: settings -->
|
||||
<!-- ## screen: cloud settings -->
|
||||
<!-- ## screen: licenses -->
|
||||
<!-- ## screen: authenticate cloud -->
|
||||
<!-- ## screen: empty dir file info -->
|
||||
<!-- ## screen: insecure android version info -->
|
||||
<!-- # dialogs -->
|
||||
<!-- Vault not found -->
|
||||
<!-- # error reports -->
|
||||
<!-- # misc -->
|
||||
<!-- ## file size helper -->
|
||||
<!-- ## date helper -->
|
||||
<!-- ## biometric authentication -->
|
||||
<!-- notification -->
|
||||
<!-- lock timeout names -->
|
||||
<!-- cache size names -->
|
||||
<!-- screen scheme mode names -->
|
||||
<!-- update interval names -->
|
||||
</resources>
|
@ -116,6 +116,7 @@
|
||||
<string name="screen_webdav_settings_msg_url_is_invalid">URL geçersiz.</string>
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Kullanıcı adı boş olamaz.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Parola boş olamaz.</string>
|
||||
<!-- ## screen: s3 settings -->
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_msg_name_empty">Kasa adı boş olamaz.</string>
|
||||
<string name="screen_enter_vault_name_vault_label">Kasa adı</string>
|
||||
@ -126,7 +127,6 @@
|
||||
<string name="screen_set_password_button_text">Bitti</string>
|
||||
<string name="screen_set_password_hint">ÖNEMLİ UYARI: Parolanızı unutursanız, verilerinizi kurtarmanın herhangi bir yolu yoktur.</string>
|
||||
<string name="screen_set_password_retype_password_label">Yeni şifreyi tekrar yazın</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Çok zayıf</string>
|
||||
<string name="screen_set_password_strength_indicator_1">Güçsüz</string>
|
||||
<string name="screen_set_password_strength_indicator_2">Makul</string>
|
||||
<string name="screen_set_password_strength_indicator_3">Kuvvetli</string>
|
||||
|
@ -33,6 +33,7 @@
|
||||
<string name="error_play_services_not_available">Play Services not installed</string>
|
||||
<string name="error_biometric_auth_aborted">Biometric authentication aborted</string>
|
||||
<string name="error_file_not_found_after_opening_using_3party">Local file isn\'t present anymore after switching back to Cryptomator. Possible changes cannot be propagated back to the cloud.</string>
|
||||
<string name="error_no_such_bucket">No such bucket</string>
|
||||
|
||||
<!-- # clouds -->
|
||||
|
||||
@ -43,6 +44,7 @@
|
||||
<string name="cloud_names_onedrive" translatable="false">OneDrive</string>
|
||||
<string name="cloud_names_pcloud" translatable="false">pCloud</string>
|
||||
<string name="cloud_names_webdav" translatable="false">WebDAV</string>
|
||||
<string name="cloud_names_s3" translatable="false">S3</string>
|
||||
<string name="cloud_names_local_storage">Local storage</string>
|
||||
|
||||
<!-- # permission -->
|
||||
@ -172,6 +174,22 @@
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Username can\'t be empty.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Password can\'t be empty.</string>
|
||||
|
||||
<!-- ## screen: s3 settings -->
|
||||
<string name="screen_s3_settings_title" translatable="false">@string/cloud_names_s3</string>
|
||||
<string name="screen_s3_settings_display_name_label">Display Name</string>
|
||||
<string name="screen_s3_settings_access_key_label">Access Key</string>
|
||||
<string name="screen_s3_settings_secret_key_label">Secret Key</string>
|
||||
<string name="screen_s3_settings_bucket_label">Existing Bucket</string>
|
||||
<string name="screen_s3_settings_endpoint_label">Endpoint</string>
|
||||
<string name="screen_s3_settings_region_label">Region</string>
|
||||
<string name="screen_s3_settings_amazon_s3_text" translatable="false">Amazon S3</string>
|
||||
|
||||
<string name="screen_s3_settings_msg_display_name_not_empty">Display Name can\'t be empty</string>
|
||||
<string name="screen_s3_settings_msg_access_key_not_empty">Access Key can\'t be empty</string>
|
||||
<string name="screen_s3_settings_msg_secret_key_not_empty">Secret Key can\'t be empty</string>
|
||||
<string name="screen_s3_settings_msg_bucket_not_empty">Bucket can\'t be empty</string>
|
||||
<string name="screen_s3_settings_msg_endpoint_and_region_not_empty">Endpoint and Region can\'t be empty</string>
|
||||
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_title" translatable="false">@string/screen_vault_list_action_create_new_vault</string>
|
||||
<string name="screen_enter_vault_name_msg_name_empty">Vault name can\'t be empty.</string>
|
||||
@ -187,7 +205,7 @@
|
||||
<string name="screen_set_password_password_label" translatable="false">@string/screen_webdav_settings_password_label</string>
|
||||
<string name="screen_set_password_retype_password_label">Retype password</string>
|
||||
|
||||
<string name="screen_set_password_strength_indicator_0">Very weak</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Too weak to create a vault</string>
|
||||
<string name="screen_set_password_strength_indicator_1">Weak</string>
|
||||
<string name="screen_set_password_strength_indicator_2">Fair</string>
|
||||
<string name="screen_set_password_strength_indicator_3">Strong</string>
|
||||
@ -257,6 +275,7 @@
|
||||
<string name="screen_cloud_settings_title" translatable="false">@string/screen_settings_cloud_settings_label</string>
|
||||
<string name="screen_cloud_settings_webdav_connections">WebDAV connections</string>
|
||||
<string name="screen_cloud_settings_pcloud_connections">pCloud connections</string>
|
||||
<string name="screen_cloud_settings_s3_connections">S3 connections</string>
|
||||
<string name="screen_cloud_settings_local_storage_locations">Local storage locations</string>
|
||||
<string name="screen_cloud_settings_log_in_to">Log in to</string>
|
||||
<string name="screen_cloud_settings_sign_out_from_cloud">Sign out from</string>
|
||||
|
@ -134,6 +134,13 @@
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://github.com/ReactiveX/RxJava" />
|
||||
</Preference>
|
||||
<Preference
|
||||
android:summary="Apache License v2"
|
||||
android:title="AWS SDK for Android">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://github.com/aws-amplify/aws-sdk-android" />
|
||||
</Preference>
|
||||
<Preference
|
||||
android:summary="Apache License v2"
|
||||
android:title="Subsampling Scale Image View">
|
||||
|
@ -40,10 +40,12 @@ import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||
import org.cryptomator.presentation.exception.PermissionNotGrantedException
|
||||
import org.cryptomator.presentation.intent.AuthenticateCloudIntent
|
||||
import org.cryptomator.presentation.intent.Intents
|
||||
import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.ProgressModel
|
||||
import org.cryptomator.presentation.model.ProgressStateModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import org.cryptomator.presentation.model.mappers.CloudModelMapper
|
||||
import org.cryptomator.presentation.ui.activity.view.AuthenticateCloudView
|
||||
@ -76,6 +78,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
OnedriveAuthStrategy(), //
|
||||
PCloudAuthStrategy(), //
|
||||
WebDAVAuthStrategy(), //
|
||||
S3AuthStrategy(), //
|
||||
LocalStorageAuthStrategy() //
|
||||
)
|
||||
|
||||
@ -448,6 +451,38 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
finish()
|
||||
}
|
||||
|
||||
private inner class S3AuthStrategy : AuthStrategy {
|
||||
|
||||
private var authenticationStarted = false
|
||||
|
||||
override fun supports(cloud: CloudModel): Boolean {
|
||||
return cloud.cloudType() == CloudTypeModel.S3
|
||||
}
|
||||
|
||||
override fun resumed(intent: AuthenticateCloudIntent) {
|
||||
when {
|
||||
ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> {
|
||||
if (!authenticationStarted) {
|
||||
startAuthentication(intent.cloud())
|
||||
Toast.makeText(
|
||||
context(),
|
||||
String.format(getString(R.string.error_authentication_failed), intent.cloud().username()),
|
||||
Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.tag("AuthicateCloudPrester").e(intent.error())
|
||||
failAuthentication(intent.cloud().name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAuthentication(cloud: CloudModel) {
|
||||
authenticationStarted = true
|
||||
startIntent(Intents.s3AddOrChangeIntent().withS3Cloud(cloud as S3CloudModel))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LocalStorageAuthStrategy : AuthStrategy {
|
||||
|
||||
private var authenticationStarted = false
|
||||
|
@ -21,7 +21,7 @@ class LruFileCacheUtil(context: Context) {
|
||||
private val parent: File = context.cacheDir
|
||||
|
||||
enum class Cache {
|
||||
DROPBOX, WEBDAV, PCLOUD, ONEDRIVE, GOOGLE_DRIVE
|
||||
DROPBOX, WEBDAV, PCLOUD, S3, ONEDRIVE, GOOGLE_DRIVE
|
||||
}
|
||||
|
||||
fun resolve(cache: Cache?): File {
|
||||
@ -29,6 +29,7 @@ class LruFileCacheUtil(context: Context) {
|
||||
Cache.DROPBOX -> File(parent, "LruCacheDropbox")
|
||||
Cache.WEBDAV -> File(parent, "LruCacheWebdav")
|
||||
Cache.PCLOUD -> File(parent, "LruCachePCloud")
|
||||
Cache.S3 -> File(parent, "LruCacheS3")
|
||||
Cache.ONEDRIVE -> File(parent, "LruCacheOneDrive")
|
||||
Cache.GOOGLE_DRIVE -> File(parent, "LruCacheGoogleDrive")
|
||||
else -> throw IllegalStateException()
|
||||
|