Merge pull request #313 from cryptomator/feature/switch-s3-dependency
Switch S3 dependency which closes #312
This commit is contained in:
commit
5192194cf7
@ -53,8 +53,6 @@ ext {
|
|||||||
// do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x
|
// do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x
|
||||||
cryptolibVersion = '2.0.0-rc1'
|
cryptolibVersion = '2.0.0-rc1'
|
||||||
|
|
||||||
awsAndroidSdkS3 = '2.23.0'
|
|
||||||
|
|
||||||
dropboxVersion = '4.0.0'
|
dropboxVersion = '4.0.0'
|
||||||
|
|
||||||
googleApiServicesVersion = 'v3-rev197-1.25.0'
|
googleApiServicesVersion = 'v3-rev197-1.25.0'
|
||||||
@ -63,6 +61,9 @@ ext {
|
|||||||
|
|
||||||
msgraphVersion = '2.10.0'
|
msgraphVersion = '2.10.0'
|
||||||
|
|
||||||
|
minIoVersion = '8.2.1'
|
||||||
|
staxVersion = '1.2.0' // needed for minIO
|
||||||
|
|
||||||
commonsCodecVersion = '1.15'
|
commonsCodecVersion = '1.15'
|
||||||
|
|
||||||
recyclerViewFastScrollVersion = '2.0.1'
|
recyclerViewFastScrollVersion = '2.0.1'
|
||||||
@ -95,8 +96,6 @@ ext {
|
|||||||
|
|
||||||
jsonWebTokenApiVersion = '0.11.2'
|
jsonWebTokenApiVersion = '0.11.2'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
android : "com.google.android:android:${androidVersion}",
|
android : "com.google.android:android:${androidVersion}",
|
||||||
androidAnnotations : "androidx.annotation:annotation:${androidSupportAnnotationsVersion}",
|
androidAnnotations : "androidx.annotation:annotation:${androidSupportAnnotationsVersion}",
|
||||||
@ -107,7 +106,6 @@ ext {
|
|||||||
androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}",
|
androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}",
|
||||||
androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}",
|
androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}",
|
||||||
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
|
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
|
||||||
awsAndroidS3 : "com.amazonaws:aws-android-sdk-s3:${awsAndroidSdkS3}",
|
|
||||||
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
|
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
|
||||||
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
|
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
|
||||||
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",
|
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",
|
||||||
@ -132,9 +130,10 @@ ext {
|
|||||||
junitParams : "org.junit.jupiter:junit-jupiter-params:${jUnitVersion}",
|
junitParams : "org.junit.jupiter:junit-jupiter-params:${jUnitVersion}",
|
||||||
junit4 : "org.junit.jupiter:junit-jupiter:${jUnit4Version}",
|
junit4 : "org.junit.jupiter:junit-jupiter:${jUnit4Version}",
|
||||||
junit4Engine : "org.junit.vintage:junit-vintage-engine:${jUnitVersion}",
|
junit4Engine : "org.junit.vintage:junit-vintage-engine:${jUnitVersion}",
|
||||||
msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}",
|
minIo : "io.minio:minio:${minIoVersion}",
|
||||||
mockito : "org.mockito:mockito-core:${mockitoVersion}",
|
mockito : "org.mockito:mockito-core:${mockitoVersion}",
|
||||||
mockitoInline : "org.mockito:mockito-inline:${mockitoInlineVersion}",
|
mockitoInline : "org.mockito:mockito-inline:${mockitoInlineVersion}",
|
||||||
|
msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}",
|
||||||
multidex : "androidx.multidex:multidex:${multidexVersion}",
|
multidex : "androidx.multidex:multidex:${multidexVersion}",
|
||||||
okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}",
|
okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}",
|
||||||
okHttpDigest : "com.burgstaller:okhttp-digest:${okHttpDigestVersion}",
|
okHttpDigest : "com.burgstaller:okhttp-digest:${okHttpDigestVersion}",
|
||||||
@ -142,6 +141,7 @@ ext {
|
|||||||
rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}",
|
rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}",
|
||||||
rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}",
|
rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}",
|
||||||
rxBinding : "com.jakewharton.rxbinding2:rxbinding:${rxBindingVersion}",
|
rxBinding : "com.jakewharton.rxbinding2:rxbinding:${rxBindingVersion}",
|
||||||
|
stax : "stax:stax:${staxVersion}",
|
||||||
testingSupportLib : "com.android.support.test:testing-support-lib:${testingSupportLibVersion}",
|
testingSupportLib : "com.android.support.test:testing-support-lib:${testingSupportLibVersion}",
|
||||||
timber : "com.jakewharton.timber:timber:${timberVersion}",
|
timber : "com.jakewharton.timber:timber:${timberVersion}",
|
||||||
velocity : "org.apache.velocity:velocity:${velocityVersion}",
|
velocity : "org.apache.velocity:velocity:${velocityVersion}",
|
||||||
|
@ -78,7 +78,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
greendao {
|
greendao {
|
||||||
schemaVersion 7
|
schemaVersion 8
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.all {
|
configurations.all {
|
||||||
@ -110,10 +110,12 @@ dependencies {
|
|||||||
implementation dependencies.jsonWebTokenJson
|
implementation dependencies.jsonWebTokenJson
|
||||||
|
|
||||||
// cloud
|
// cloud
|
||||||
implementation dependencies.awsAndroidS3
|
|
||||||
implementation dependencies.dropbox
|
implementation dependencies.dropbox
|
||||||
implementation dependencies.msgraph
|
implementation dependencies.msgraph
|
||||||
|
|
||||||
|
implementation dependencies.stax
|
||||||
|
compile dependencies.minIo
|
||||||
|
|
||||||
playstoreImplementation dependencies.googlePlayServicesAuth
|
playstoreImplementation dependencies.googlePlayServicesAuth
|
||||||
apkstoreImplementation dependencies.googlePlayServicesAuth
|
apkstoreImplementation dependencies.googlePlayServicesAuth
|
||||||
|
|
||||||
|
@ -1,51 +1,93 @@
|
|||||||
package org.cryptomator.data.cloud.s3;
|
package org.cryptomator.data.cloud.s3;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
|
||||||
import com.amazonaws.Request;
|
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor;
|
||||||
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.domain.S3Cloud;
|
||||||
|
import org.cryptomator.util.SharedPreferencesHandler;
|
||||||
import org.cryptomator.util.crypto.CredentialCryptor;
|
import org.cryptomator.util.crypto.CredentialCryptor;
|
||||||
|
import org.cryptomator.util.file.LruFileCacheUtil;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import okhttp3.Cache;
|
||||||
|
import okhttp3.CacheControl;
|
||||||
|
import okhttp3.Interceptor;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static org.cryptomator.data.util.NetworkTimeout.CONNECTION;
|
||||||
|
import static org.cryptomator.data.util.NetworkTimeout.READ;
|
||||||
|
import static org.cryptomator.data.util.NetworkTimeout.WRITE;
|
||||||
|
import static org.cryptomator.util.file.LruFileCacheUtil.Cache.S3;
|
||||||
|
|
||||||
class S3ClientFactory {
|
class S3ClientFactory {
|
||||||
|
|
||||||
private AmazonS3 apiClient;
|
private MinioClient apiClient;
|
||||||
|
|
||||||
public AmazonS3 getClient(S3Cloud cloud, Context context) {
|
private static Interceptor httpLoggingInterceptor(Context context) {
|
||||||
|
return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MinioClient getClient(S3Cloud cloud, Context context) {
|
||||||
if (apiClient == null) {
|
if (apiClient == null) {
|
||||||
apiClient = createApiClient(cloud, context);
|
apiClient = createApiClient(cloud, context);
|
||||||
}
|
}
|
||||||
return apiClient;
|
return apiClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AmazonS3 createApiClient(S3Cloud cloud, Context context) {
|
private MinioClient createApiClient(S3Cloud cloud, Context context) {
|
||||||
Region region = Region.getRegion(Regions.DEFAULT_REGION);
|
final SharedPreferencesHandler sharedPreferencesHandler = new SharedPreferencesHandler(context);
|
||||||
String endpoint = null;
|
|
||||||
|
|
||||||
if (cloud.s3Region() != null) {
|
MinioClient.Builder minioClientBuilder = MinioClient.builder();
|
||||||
region = Region.getRegion(cloud.s3Region());
|
|
||||||
} else if (cloud.s3Endpoint() != null) {
|
minioClientBuilder.endpoint(cloud.s3Endpoint());
|
||||||
endpoint = cloud.s3Endpoint();
|
minioClientBuilder.region(cloud.s3Region());
|
||||||
|
|
||||||
|
OkHttpClient.Builder httpClientBuilder = new OkHttpClient() //
|
||||||
|
.newBuilder() //
|
||||||
|
.connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) //
|
||||||
|
.readTimeout(READ.getTimeout(), READ.getUnit()) //
|
||||||
|
.writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) //
|
||||||
|
.addInterceptor(httpLoggingInterceptor(context));
|
||||||
|
|
||||||
|
if (sharedPreferencesHandler.useLruCache()) {
|
||||||
|
final Cache cache = new Cache(new LruFileCacheUtil(context).resolve(S3), sharedPreferencesHandler.lruCacheSize());
|
||||||
|
httpClientBuilder.cache(cache).addInterceptor(provideOfflineCacheInterceptor(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
AmazonS3Client client = new AmazonS3Client(new BasicAWSCredentials(decrypt(cloud.accessKey(), context), decrypt(cloud.secretKey(), context)), region);
|
return minioClientBuilder //
|
||||||
|
.credentials(decrypt(cloud.accessKey(), context), decrypt(cloud.secretKey(), context)) //
|
||||||
|
.httpClient(httpClientBuilder.build()) //
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
if (endpoint != null) {
|
private static Interceptor provideOfflineCacheInterceptor(final Context context) {
|
||||||
client.setEndpoint(cloud.s3Endpoint());
|
return chain -> {
|
||||||
}
|
Request request = chain.request();
|
||||||
|
|
||||||
client.addRequestHandler(new LoggingAwareRequestHandler());
|
if (isNetworkAvailable(context)) {
|
||||||
|
final CacheControl cacheControl = new CacheControl.Builder() //
|
||||||
|
.maxAge(0, TimeUnit.DAYS) //
|
||||||
|
.build();
|
||||||
|
|
||||||
return client;
|
request = request.newBuilder() //
|
||||||
|
.cacheControl(cacheControl) //
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.proceed(request);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isNetworkAvailable(final Context context) {
|
||||||
|
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
|
||||||
|
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String decrypt(String password, Context context) {
|
private String decrypt(String password, Context context) {
|
||||||
@ -53,35 +95,4 @@ class S3ClientFactory {
|
|||||||
.getInstance(context) //
|
.getInstance(context) //
|
||||||
.decrypt(password);
|
.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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,11 @@ package org.cryptomator.data.cloud.s3;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import com.amazonaws.services.s3.model.AmazonS3Exception;
|
|
||||||
|
|
||||||
import org.cryptomator.data.cloud.InterceptingCloudContentRepository;
|
import org.cryptomator.data.cloud.InterceptingCloudContentRepository;
|
||||||
import org.cryptomator.domain.S3Cloud;
|
import org.cryptomator.domain.S3Cloud;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
import org.cryptomator.domain.exception.FatalBackendException;
|
||||||
|
import org.cryptomator.domain.exception.ForbiddenException;
|
||||||
import org.cryptomator.domain.exception.NetworkConnectionException;
|
import org.cryptomator.domain.exception.NetworkConnectionException;
|
||||||
import org.cryptomator.domain.exception.NoSuchBucketException;
|
import org.cryptomator.domain.exception.NoSuchBucketException;
|
||||||
import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
|
import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
|
||||||
@ -23,6 +22,8 @@ import java.io.IOException;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.minio.errors.ErrorResponseException;
|
||||||
|
|
||||||
import static org.cryptomator.util.ExceptionUtil.contains;
|
import static org.cryptomator.util.ExceptionUtil.contains;
|
||||||
|
|
||||||
class S3CloudContentRepository extends InterceptingCloudContentRepository<S3Cloud, S3Node, S3Folder, S3File> {
|
class S3CloudContentRepository extends InterceptingCloudContentRepository<S3Cloud, S3Node, S3Folder, S3File> {
|
||||||
@ -42,9 +43,9 @@ class S3CloudContentRepository extends InterceptingCloudContentRepository<S3Clou
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void throwNoSuchBucketExceptionIfRequired(Exception e) throws NoSuchBucketException {
|
private void throwNoSuchBucketExceptionIfRequired(Exception e) throws NoSuchBucketException {
|
||||||
if (e instanceof AmazonS3Exception) {
|
if (e instanceof ErrorResponseException) {
|
||||||
String errorCode = ((AmazonS3Exception)e).getErrorCode();
|
String errorCode = ((ErrorResponseException) e).errorResponse().code();
|
||||||
if(S3CloudApiExceptions.isNoSuchBucketException(errorCode)) {
|
if (S3CloudApiExceptions.isNoSuchBucketException(errorCode)) {
|
||||||
throw new NoSuchBucketException(cloud.s3Bucket());
|
throw new NoSuchBucketException(cloud.s3Bucket());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,11 +58,13 @@ class S3CloudContentRepository extends InterceptingCloudContentRepository<S3Clou
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void throwWrongCredentialsExceptionIfRequired(Exception e) {
|
private void throwWrongCredentialsExceptionIfRequired(Exception e) {
|
||||||
if (e instanceof AmazonS3Exception) {
|
if (e instanceof ErrorResponseException) {
|
||||||
String errorCode = ((AmazonS3Exception) e).getErrorCode();
|
String errorCode = ((ErrorResponseException) e).errorResponse().code();
|
||||||
if (S3CloudApiExceptions.isAccessProblem(errorCode)) {
|
if (S3CloudApiExceptions.isAccessProblem(errorCode)) {
|
||||||
throw new WrongCredentialsException(cloud);
|
throw new WrongCredentialsException(cloud);
|
||||||
}
|
}
|
||||||
|
} else if(e instanceof ForbiddenException) {
|
||||||
|
throw new WrongCredentialsException(cloud);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +82,7 @@ class S3CloudContentRepository extends InterceptingCloudContentRepository<S3Clou
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public S3Folder resolve(S3Cloud cloud, String path) throws BackendException {
|
public S3Folder resolve(S3Cloud cloud, String path) throws BackendException {
|
||||||
return this.cloud.resolve(path);
|
return this.cloud.resolve(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -107,7 +110,7 @@ class S3CloudContentRepository extends InterceptingCloudContentRepository<S3Clou
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean exists(S3Node node) throws BackendException {
|
public boolean exists(S3Node node) throws BackendException {
|
||||||
return cloud.exists(node);
|
return cloud.exists(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -158,7 +161,7 @@ class S3CloudContentRepository extends InterceptingCloudContentRepository<S3Clou
|
|||||||
@Override
|
@Override
|
||||||
public void read(S3File file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
public void read(S3File file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
||||||
try {
|
try {
|
||||||
cloud.read(file, encryptedTmpFile, data, progressAware);
|
cloud.read(file, data, progressAware);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new FatalBackendException(e);
|
throw new FatalBackendException(e);
|
||||||
}
|
}
|
||||||
@ -175,7 +178,7 @@ class S3CloudContentRepository extends InterceptingCloudContentRepository<S3Clou
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String checkAuthenticationAndRetrieveCurrentAccount(S3Cloud cloud) throws BackendException {
|
public String checkAuthenticationAndRetrieveCurrentAccount(S3Cloud cloud) throws BackendException {
|
||||||
return this.cloud.checkAuthenticationAndRetrieveCurrentAccount();
|
return this.cloud.checkAuthentication();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
package org.cryptomator.data.cloud.s3;
|
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 org.cryptomator.util.Optional;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -11,16 +8,10 @@ class S3CloudNodeFactory {
|
|||||||
|
|
||||||
private static final String DELIMITER = "/";
|
private static final String DELIMITER = "/";
|
||||||
|
|
||||||
public static S3File file(S3Folder parent, S3ObjectSummary file) {
|
public static S3File file(S3Folder parent, String name) {
|
||||||
String name = getNameFromKey(file.getKey());
|
return new S3File(parent, name, getNodePath(parent, name), Optional.empty(), Optional.empty());
|
||||||
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) {
|
public static S3File file(S3Folder parent, String name, Optional<Long> size) {
|
||||||
return new S3File(parent, name, getNodePath(parent, name), size, Optional.empty());
|
return new S3File(parent, name, getNodePath(parent, name), size, Optional.empty());
|
||||||
}
|
}
|
||||||
@ -33,11 +24,6 @@ class S3CloudNodeFactory {
|
|||||||
return new S3File(parent, name, getNodePath(parent, name), size, 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) {
|
public static S3Folder folder(S3Folder parent, String name) {
|
||||||
return new S3Folder(parent, name, getNodePath(parent, name));
|
return new S3Folder(parent, name, getNodePath(parent, name));
|
||||||
}
|
}
|
||||||
@ -47,23 +33,15 @@ class S3CloudNodeFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String getNodePath(S3Folder parent, String name) {
|
private static String getNodePath(S3Folder parent, String name) {
|
||||||
return parent.getKey() + name;
|
return parent.getPath() + "/" + name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getNameFromKey(String key) {
|
public static String getNameFromKey(String key) {
|
||||||
String name = key;
|
String name = key;
|
||||||
if (key.endsWith(DELIMITER)) {
|
if (key.endsWith(DELIMITER)) {
|
||||||
name = key.substring(0, key.length() -1);
|
name = key.substring(0, key.length() - 1);
|
||||||
}
|
}
|
||||||
return name.contains(DELIMITER) ? name.substring(name.lastIndexOf(DELIMITER) + 1) : name;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ import java.util.Date;
|
|||||||
|
|
||||||
class S3File implements CloudFile, S3Node {
|
class S3File implements CloudFile, S3Node {
|
||||||
|
|
||||||
|
private static final String DELIMITER = "/";
|
||||||
|
|
||||||
private final S3Folder parent;
|
private final S3Folder parent;
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String path;
|
private final String path;
|
||||||
@ -39,6 +41,9 @@ class S3File implements CloudFile, S3Node {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getKey() {
|
public String getKey() {
|
||||||
|
if (path.startsWith(DELIMITER)) {
|
||||||
|
return path.substring(DELIMITER.length());
|
||||||
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +34,9 @@ class S3Folder implements CloudFolder, S3Node {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getKey() {
|
public String getKey() {
|
||||||
|
if (path.startsWith(DELIMITER)) {
|
||||||
|
return path.substring(DELIMITER.length()) + DELIMITER;
|
||||||
|
}
|
||||||
return path + DELIMITER;
|
return path + DELIMITER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,27 +2,9 @@ package org.cryptomator.data.cloud.s3;
|
|||||||
|
|
||||||
import android.content.Context;
|
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.data.util.CopyStream;
|
||||||
|
import org.cryptomator.data.util.TransferredBytesAwareInputStream;
|
||||||
|
import org.cryptomator.data.util.TransferredBytesAwareOutputStream;
|
||||||
import org.cryptomator.domain.S3Cloud;
|
import org.cryptomator.domain.S3Cloud;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
|
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
|
||||||
@ -30,7 +12,6 @@ import org.cryptomator.domain.exception.FatalBackendException;
|
|||||||
import org.cryptomator.domain.exception.ForbiddenException;
|
import org.cryptomator.domain.exception.ForbiddenException;
|
||||||
import org.cryptomator.domain.exception.NoSuchBucketException;
|
import org.cryptomator.domain.exception.NoSuchBucketException;
|
||||||
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
||||||
import org.cryptomator.domain.exception.UnauthorizedException;
|
|
||||||
import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
|
import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
|
||||||
import org.cryptomator.domain.usecases.ProgressAware;
|
import org.cryptomator.domain.usecases.ProgressAware;
|
||||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
import org.cryptomator.domain.usecases.cloud.DataSource;
|
||||||
@ -38,31 +19,40 @@ import org.cryptomator.domain.usecases.cloud.DownloadState;
|
|||||||
import org.cryptomator.domain.usecases.cloud.Progress;
|
import org.cryptomator.domain.usecases.cloud.Progress;
|
||||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
import org.cryptomator.domain.usecases.cloud.UploadState;
|
||||||
import org.cryptomator.util.Optional;
|
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.ByteArrayInputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
|
|
||||||
|
import io.minio.BucketExistsArgs;
|
||||||
|
import io.minio.CopyObjectArgs;
|
||||||
|
import io.minio.CopySource;
|
||||||
|
import io.minio.GetObjectArgs;
|
||||||
|
import io.minio.GetObjectResponse;
|
||||||
|
import io.minio.ListObjectsArgs;
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import io.minio.ObjectWriteResponse;
|
||||||
|
import io.minio.PutObjectArgs;
|
||||||
|
import io.minio.RemoveObjectArgs;
|
||||||
|
import io.minio.RemoveObjectsArgs;
|
||||||
|
import io.minio.Result;
|
||||||
|
import io.minio.StatObjectArgs;
|
||||||
|
import io.minio.StatObjectResponse;
|
||||||
|
import io.minio.errors.ErrorResponseException;
|
||||||
|
import io.minio.messages.DeleteError;
|
||||||
|
import io.minio.messages.DeleteObject;
|
||||||
|
import io.minio.messages.Item;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
|
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 {
|
class S3Impl {
|
||||||
|
|
||||||
private static final long CHUNKED_UPLOAD_MAX_SIZE = 100L << 20;
|
|
||||||
private static final String DELIMITER = "/";
|
private static final String DELIMITER = "/";
|
||||||
|
|
||||||
private final S3ClientFactory clientFactory = new S3ClientFactory();
|
private final S3ClientFactory clientFactory = new S3ClientFactory();
|
||||||
@ -70,9 +60,6 @@ class S3Impl {
|
|||||||
private final RootS3Folder root;
|
private final RootS3Folder root;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
private final SharedPreferencesHandler sharedPreferencesHandler;
|
|
||||||
private DiskLruCache diskLruCache;
|
|
||||||
|
|
||||||
S3Impl(Context context, S3Cloud cloud) {
|
S3Impl(Context context, S3Cloud cloud) {
|
||||||
if (cloud.accessKey() == null || cloud.secretKey() == null) {
|
if (cloud.accessKey() == null || cloud.secretKey() == null) {
|
||||||
throw new WrongCredentialsException(cloud);
|
throw new WrongCredentialsException(cloud);
|
||||||
@ -81,10 +68,9 @@ class S3Impl {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
this.cloud = cloud;
|
this.cloud = cloud;
|
||||||
this.root = new RootS3Folder(cloud);
|
this.root = new RootS3Folder(cloud);
|
||||||
this.sharedPreferencesHandler = new SharedPreferencesHandler(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AmazonS3 client() {
|
private MinioClient client() {
|
||||||
return clientFactory.getClient(cloud, context);
|
return clientFactory.getClient(cloud, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,31 +104,48 @@ class S3Impl {
|
|||||||
return S3CloudNodeFactory.folder(parent, name, parent.getKey() + name);
|
return S3CloudNodeFactory.folder(parent, name, parent.getKey() + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean exists(S3Node node) {
|
public boolean exists(S3Node node) throws BackendException {
|
||||||
String key = node.getKey();
|
String key = node.getKey();
|
||||||
|
try {
|
||||||
|
if(!(node instanceof RootS3Folder)) {
|
||||||
|
client().statObject(StatObjectArgs.builder().bucket(cloud.s3Bucket()).object(key).build());
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// stat requests throws an IllegalStateException if key is empty string
|
||||||
|
ListObjectsArgs request = ListObjectsArgs.builder().bucket(cloud.s3Bucket()).prefix(key).delimiter(DELIMITER).build();
|
||||||
|
return client().listObjects(request).iterator().hasNext();
|
||||||
|
}
|
||||||
|
} catch (ErrorResponseException e) {
|
||||||
|
if (S3CloudApiErrorCodes.NO_SUCH_KEY.getValue().equals(e.errorResponse().code())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw new FatalBackendException(e);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
handleApiError(ex, node.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
ListObjectsV2Result result = client().listObjectsV2(cloud.s3Bucket(), key);
|
throw new FatalBackendException(new IllegalStateException("Exception thrown but not handled?"));
|
||||||
|
|
||||||
return result.getObjectSummaries().size() > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<S3Node> list(S3Folder folder) throws IOException, BackendException {
|
public List<S3Node> list(S3Folder folder) throws IOException, BackendException {
|
||||||
List<S3Node> result = new ArrayList<>();
|
List<S3Node> result = new ArrayList<>();
|
||||||
|
|
||||||
ListObjectsV2Request request = new ListObjectsV2Request().withBucketName(cloud.s3Bucket()).withPrefix(folder.getKey()).withDelimiter(DELIMITER);
|
ListObjectsArgs request = ListObjectsArgs.builder().bucket(cloud.s3Bucket()).prefix(folder.getKey()).delimiter(DELIMITER).build();
|
||||||
|
Iterable<Result<Item>> listObjects = client().listObjects(request);
|
||||||
ListObjectsV2Result listObjects = client().listObjectsV2(request);
|
for (Result<Item> object : listObjects) {
|
||||||
for (String prefix : listObjects.getCommonPrefixes()) {
|
try {
|
||||||
// add folders
|
Item item = object.get();
|
||||||
result.add(S3CloudNodeFactory.folder(folder, S3CloudNodeFactory.getNameFromKey(prefix)));
|
if (item.isDir()) {
|
||||||
}
|
result.add(S3CloudNodeFactory.folder(folder, S3CloudNodeFactory.getNameFromKey(item.objectName())));
|
||||||
|
} else {
|
||||||
for (S3ObjectSummary objectSummary : listObjects.getObjectSummaries()) {
|
S3File file = S3CloudNodeFactory.file(folder, S3CloudNodeFactory.getNameFromKey(item.objectName()), Optional.of(item.size()), Optional.of(Date.from(item.lastModified().toInstant())));
|
||||||
// add files but skip parent folder
|
result.add(file);
|
||||||
if (!objectSummary.getKey().equals(listObjects.getPrefix())) {
|
}
|
||||||
result.add(S3CloudNodeFactory.file(folder, objectSummary));
|
} catch (Exception ex) {
|
||||||
|
handleApiError(ex, folder.getPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,16 +157,17 @@ class S3Impl {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectMetadata metadata = new ObjectMetadata();
|
|
||||||
metadata.setContentLength(0);
|
|
||||||
|
|
||||||
InputStream emptyContent = new ByteArrayInputStream(new byte[0]);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PutObjectRequest putObjectRequest = new PutObjectRequest(cloud.s3Bucket(), folder.getKey(), emptyContent, metadata);
|
PutObjectArgs putObjectArgs = PutObjectArgs //
|
||||||
client().putObject(putObjectRequest);
|
.builder() //
|
||||||
} catch(AmazonS3Exception ex) {
|
.bucket(cloud.s3Bucket()) //
|
||||||
handleApiError(ex, folder.getName());
|
.object(folder.getKey()) //
|
||||||
|
.stream(new ByteArrayInputStream(new byte[0]), 0, -1) //
|
||||||
|
.build();
|
||||||
|
|
||||||
|
client().putObject(putObjectArgs);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
handleApiError(ex, folder.getPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
return S3CloudNodeFactory.folder(folder.getParent(), folder.getName());
|
return S3CloudNodeFactory.folder(folder.getParent(), folder.getName());
|
||||||
@ -175,28 +179,58 @@ class S3Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (source instanceof S3Folder) {
|
if (source instanceof S3Folder) {
|
||||||
ListObjectsV2Result listObjects = client().listObjectsV2(cloud.s3Bucket(), source.getPath());
|
List<S3Node> nodes = list((S3Folder) source);
|
||||||
|
|
||||||
if (listObjects.getObjectSummaries().size() > 0) {
|
List<DeleteObject> objectsToDelete = new LinkedList<>();
|
||||||
|
|
||||||
List<DeleteObjectsRequest.KeyVersion> objectsToDelete = new ArrayList<>();
|
for (S3Node node : nodes) {
|
||||||
|
objectsToDelete.add(new DeleteObject(node.getKey()));
|
||||||
|
|
||||||
for (S3ObjectSummary summary : listObjects.getObjectSummaries()) {
|
String targetKey;
|
||||||
objectsToDelete.add(new DeleteObjectsRequest.KeyVersion(summary.getKey()));
|
if (node instanceof S3Folder) {
|
||||||
String destinationKey = summary.getKey().replace(source.getPath(), target.getPath());
|
targetKey = S3CloudNodeFactory.folder((S3Folder) target, node.getName()).getKey();
|
||||||
|
} else {
|
||||||
client().copyObject(cloud.s3Bucket(), summary.getKey(), cloud.s3Bucket(), destinationKey);
|
targetKey = S3CloudNodeFactory.file((S3Folder) target, node.getName()).getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
CopySource copySource = CopySource.builder().bucket(cloud.s3Bucket()).object(node.getKey()).build();
|
||||||
|
|
||||||
|
CopyObjectArgs copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).object(targetKey).source(copySource).build();
|
||||||
|
try {
|
||||||
|
client().copyObject(copyObjectArgs);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
handleApiError(ex, source.getPath());
|
||||||
}
|
}
|
||||||
client().deleteObjects(new DeleteObjectsRequest(cloud.s3Bucket()).withKeys(objectsToDelete));
|
|
||||||
} else {
|
|
||||||
throw new NoSuchCloudFileException(source.getPath());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(cloud.s3Bucket()).objects(objectsToDelete).build();
|
||||||
|
|
||||||
|
for (Result<DeleteError> result : client().removeObjects(removeObjectsArgs)) {
|
||||||
|
try {
|
||||||
|
result.get();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
handleApiError(ex, source.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return S3CloudNodeFactory.folder(target.getParent(), target.getName());
|
return S3CloudNodeFactory.folder(target.getParent(), target.getName());
|
||||||
} else {
|
} else {
|
||||||
CopyObjectResult result = client().copyObject(cloud.s3Bucket(), source.getPath(), cloud.s3Bucket(), target.getPath());
|
CopySource copySource = CopySource.builder().bucket(cloud.s3Bucket()).object(source.getKey()).build();
|
||||||
client().deleteObject(cloud.s3Bucket(), source.getPath());
|
CopyObjectArgs copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).object(target.getKey()).source(copySource).build();
|
||||||
return S3CloudNodeFactory.file(target.getParent(), target.getName(), ((S3File) source).getSize(), Optional.of(result.getLastModifiedDate()));
|
try {
|
||||||
|
ObjectWriteResponse result = client().copyObject(copyObjectArgs);
|
||||||
|
|
||||||
|
delete(source);
|
||||||
|
|
||||||
|
Date lastModified = result.headers().getDate("Last-Modified");
|
||||||
|
|
||||||
|
return S3CloudNodeFactory.file(target.getParent(), target.getName(), ((S3File) source).getSize(), Optional.ofNullable(lastModified));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
handleApiError(ex, source.getPath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new FatalBackendException(new IllegalStateException("Exception thrown but not handled?"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public S3File write(S3File file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, long size) throws IOException, BackendException {
|
public S3File write(S3File file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, long size) throws IOException, BackendException {
|
||||||
@ -206,212 +240,165 @@ class S3Impl {
|
|||||||
|
|
||||||
progressAware.onProgress(Progress.started(UploadState.upload(file)));
|
progressAware.onProgress(Progress.started(UploadState.upload(file)));
|
||||||
|
|
||||||
final CompletableFuture<Optional<ObjectMetadata>> result = new CompletableFuture<>();
|
try (TransferredBytesAwareDataSource out = new TransferredBytesAwareDataSource(data) {
|
||||||
|
|
||||||
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
|
@Override
|
||||||
public void onStateChanged(int id, TransferState state) {
|
public void bytesTransferred(long transferred) {
|
||||||
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( //
|
progressAware.onProgress( //
|
||||||
progress(UploadState.upload(file)) //
|
progress(UploadState.upload(file)) //
|
||||||
.between(0) //
|
.between(0) //
|
||||||
.and(size) //
|
.and(size) //
|
||||||
.withValue(bytesCurrent));
|
.withValue(transferred));
|
||||||
}
|
}
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(cloud.s3Bucket()).object(file.getKey()).stream(out.open(context), data.size(context).get(), -1).build();
|
||||||
|
ObjectWriteResponse objectWriteResponse = client().putObject(putObjectArgs);
|
||||||
|
|
||||||
@Override
|
Date lastModified = objectWriteResponse.headers().getDate("Last-Modified");
|
||||||
public void onError(int id, Exception ex) {
|
|
||||||
result.fail(ex);
|
if(lastModified == null) {
|
||||||
|
StatObjectResponse statObjectResponse = client().statObject(StatObjectArgs //
|
||||||
|
.builder() //
|
||||||
|
.bucket(cloud.s3Bucket()) //
|
||||||
|
.object(file.getKey()) //
|
||||||
|
.build());
|
||||||
|
|
||||||
|
lastModified = Date.from(statObjectResponse.lastModified().toInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
|
||||||
|
return S3CloudNodeFactory.file(file.getParent(), file.getName(), Optional.of(size), Optional.of(lastModified));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
handleApiError(ex, file.getPath());
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
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()) {
|
throw new FatalBackendException(new IllegalStateException("Exception thrown but not handled?"));
|
||||||
try {
|
}
|
||||||
retrieveFromLruCache(cacheFile.get(), data);
|
|
||||||
} catch (IOException e) {
|
public void read(S3File file, OutputStream data, final ProgressAware<DownloadState> progressAware) throws IOException, BackendException {
|
||||||
Timber.tag("S3Impl").w(e, "Error while retrieving content from Cache, get from web request");
|
progressAware.onProgress(Progress.started(DownloadState.download(file)));
|
||||||
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware);
|
|
||||||
}
|
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(cloud.s3Bucket()).object(file.getKey()).build();
|
||||||
} else {
|
|
||||||
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware);
|
try (GetObjectResponse response = client().getObject(getObjectArgs); //
|
||||||
|
TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) {
|
||||||
|
@Override
|
||||||
|
public void bytesTransferred(long transferred) {
|
||||||
|
progressAware.onProgress( //
|
||||||
|
progress(DownloadState.download(file)) //
|
||||||
|
.between(0) //
|
||||||
|
.and(file.getSize().orElse(Long.MAX_VALUE)) //
|
||||||
|
.withValue(transferred));
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
CopyStream.copyStreamToStream(response, out);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
handleApiError(ex, file.getPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
progressAware.onProgress(Progress.completed(DownloadState.download(file)));
|
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 {
|
public void delete(S3Node node) throws IOException, BackendException {
|
||||||
if (node instanceof S3Folder) {
|
if (node instanceof S3Folder) {
|
||||||
List<S3ObjectSummary> summaries = client().listObjectsV2(cloud.s3Bucket(), node.getPath()).getObjectSummaries();
|
|
||||||
|
|
||||||
List<KeyVersion> keys = new ArrayList<>();
|
List<DeleteObject> objectsToDelete = new LinkedList<>();
|
||||||
for (S3ObjectSummary summary : summaries) {
|
|
||||||
keys.add(new KeyVersion(summary.getKey()));
|
ListObjectsArgs request = ListObjectsArgs.builder().bucket(cloud.s3Bucket()).prefix(node.getKey()).recursive(true).delimiter(DELIMITER).build();
|
||||||
|
Iterable<Result<Item>> listObjects = client().listObjects(request);
|
||||||
|
for (Result<Item> object : listObjects) {
|
||||||
|
try {
|
||||||
|
Item item = object.get();
|
||||||
|
objectsToDelete.add(new DeleteObject(item.objectName()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleApiError(e, node.getPath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteObjectsRequest request = new DeleteObjectsRequest(cloud.s3Bucket());
|
RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(cloud.s3Bucket()).objects(objectsToDelete).build();
|
||||||
request.withKeys(keys);
|
Iterable<Result<DeleteError>> results = client().removeObjects(removeObjectsArgs);
|
||||||
|
for (Result<DeleteError> result : results) {
|
||||||
|
try {
|
||||||
|
DeleteError error = result.get();
|
||||||
|
Timber.tag("S3Impl").e("Error in deleting object " + error.objectName() + "; " + error.message());
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleApiError(e, node.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
client().deleteObjects(request);
|
|
||||||
} else {
|
} else {
|
||||||
client().deleteObject(cloud.s3Bucket(), node.getPath());
|
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(cloud.s3Bucket()).object(node.getKey()).build();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(S3), cacheSize);
|
client().removeObject(removeObjectArgs);
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
Timber.tag("S3Impl").e(e, "Failed to setup LRU cache");
|
handleApiError(e, "");
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleApiError(AmazonS3Exception ex, String name) throws BackendException {
|
public String checkAuthentication() throws NoSuchBucketException, BackendException {
|
||||||
String errorCode = ex.getErrorCode();
|
try {
|
||||||
if (S3CloudApiExceptions.isAccessProblem(errorCode)) {
|
if (!client().bucketExists(BucketExistsArgs.builder().bucket(cloud.s3Bucket()).build())) {
|
||||||
throw new ForbiddenException();
|
throw new NoSuchBucketException(cloud.s3Bucket());
|
||||||
} else if (S3CloudApiErrorCodes.NO_SUCH_BUCKET.getValue().equals(errorCode)) {
|
}
|
||||||
throw new NoSuchBucketException(name);
|
} catch (Exception e) {
|
||||||
} else if (S3CloudApiErrorCodes.NO_SUCH_KEY.getValue().equals(errorCode)) {
|
handleApiError(e, "");
|
||||||
throw new NoSuchCloudFileException(name);
|
}
|
||||||
} else {
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleApiError(Exception ex, String name) throws BackendException {
|
||||||
|
if (ex instanceof ErrorResponseException) {
|
||||||
|
String errorCode = ((ErrorResponseException) ex).errorResponse().code();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
throw new FatalBackendException(ex);
|
throw new FatalBackendException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static abstract class TransferredBytesAwareDataSource implements DataSource {
|
||||||
|
|
||||||
|
private final DataSource data;
|
||||||
|
|
||||||
|
TransferredBytesAwareDataSource(DataSource data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Long> size(Context context) {
|
||||||
|
return data.size(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream open(Context context) throws IOException {
|
||||||
|
return new TransferredBytesAwareInputStream(data.open(context)) {
|
||||||
|
@Override
|
||||||
|
public void bytesTransferred(long transferred) {
|
||||||
|
S3Impl.TransferredBytesAwareDataSource.this.bytesTransferred(transferred);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void bytesTransferred(long transferred);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataSource decorate(DataSource delegate) {
|
||||||
|
return delegate;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,8 @@ class DatabaseUpgrades {
|
|||||||
Upgrade3To4 upgrade3To4, //
|
Upgrade3To4 upgrade3To4, //
|
||||||
Upgrade4To5 upgrade4To5, //
|
Upgrade4To5 upgrade4To5, //
|
||||||
Upgrade5To6 upgrade5To6, //
|
Upgrade5To6 upgrade5To6, //
|
||||||
Upgrade6To7 upgrade6To7) {
|
Upgrade6To7 upgrade6To7, //
|
||||||
|
Upgrade7To8 upgrade7To8) {
|
||||||
|
|
||||||
availableUpgrades = defineUpgrades( //
|
availableUpgrades = defineUpgrades( //
|
||||||
upgrade0To1, //
|
upgrade0To1, //
|
||||||
@ -34,7 +35,8 @@ class DatabaseUpgrades {
|
|||||||
upgrade3To4, //
|
upgrade3To4, //
|
||||||
upgrade4To5, //
|
upgrade4To5, //
|
||||||
upgrade5To6, //
|
upgrade5To6, //
|
||||||
upgrade6To7);
|
upgrade6To7, //
|
||||||
|
upgrade7To8);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Comparator<DatabaseUpgrade> reverseOrder() {
|
private static Comparator<DatabaseUpgrade> reverseOrder() {
|
||||||
|
@ -498,27 +498,26 @@ class Sql {
|
|||||||
|
|
||||||
public static class SqlDeleteBuilder {
|
public static class SqlDeleteBuilder {
|
||||||
|
|
||||||
private final String table;
|
private final String tableName;
|
||||||
private String whereClause;
|
|
||||||
private String[] whereArgs;
|
|
||||||
|
|
||||||
public SqlDeleteBuilder(String table) {
|
private final StringBuilder whereClause = new StringBuilder();
|
||||||
this.table = table;
|
private final List<String> whereArgs = new ArrayList<>();
|
||||||
|
|
||||||
|
public SqlDeleteBuilder(String tableName) {
|
||||||
|
this.tableName = tableName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SqlDeleteBuilder whereClause(String whereClause) {
|
public SqlDeleteBuilder where(String column, Criterion criterion) {
|
||||||
this.whereClause = whereClause;
|
if (whereClause.length() > 0) {
|
||||||
return this;
|
whereClause.append(" AND ");
|
||||||
}
|
}
|
||||||
|
criterion.appendTo(column, whereClause, whereArgs);
|
||||||
public SqlDeleteBuilder whereArgs(String[] whereArgs) {
|
|
||||||
this.whereArgs = whereArgs;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void executeOn(Database wrapped) {
|
public void executeOn(Database wrapped) {
|
||||||
SQLiteDatabase db = unwrap(wrapped);
|
SQLiteDatabase db = unwrap(wrapped);
|
||||||
db.delete(table, whereClause, whereArgs);
|
db.delete(tableName, whereClause.toString(), whereArgs.toArray(new String[whereArgs.size()]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
32
data/src/main/java/org/cryptomator/data/db/Upgrade7To8.kt
Normal file
32
data/src/main/java/org/cryptomator/data/db/Upgrade7To8.kt
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package org.cryptomator.data.db
|
||||||
|
|
||||||
|
import org.greenrobot.greendao.database.Database
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class Upgrade7To8 @Inject constructor() : DatabaseUpgrade(7, 8) {
|
||||||
|
|
||||||
|
override fun internalApplyTo(db: Database, origin: Int) {
|
||||||
|
db.beginTransaction()
|
||||||
|
try {
|
||||||
|
dropS3Vaults(db)
|
||||||
|
dropS3Clouds(db)
|
||||||
|
db.setTransactionSuccessful()
|
||||||
|
} finally {
|
||||||
|
db.endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dropS3Vaults(db: Database) {
|
||||||
|
Sql.deleteFrom("VAULT_ENTITY") //
|
||||||
|
.where("CLOUD_TYPE", Sql.eq("S3"))
|
||||||
|
.executeOn(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dropS3Clouds(db: Database) {
|
||||||
|
Sql.deleteFrom("CLOUD_ENTITY") //
|
||||||
|
.where("TYPE", Sql.eq("S3"))
|
||||||
|
.executeOn(db)
|
||||||
|
}
|
||||||
|
}
|
@ -35,7 +35,7 @@ class S3AddOrChangePresenter @Inject internal constructor( //
|
|||||||
if (displayName.isEmpty()) {
|
if (displayName.isEmpty()) {
|
||||||
statusMessage = getString(R.string.screen_s3_settings_msg_display_name_not_empty)
|
statusMessage = getString(R.string.screen_s3_settings_msg_display_name_not_empty)
|
||||||
}
|
}
|
||||||
if (endpoint.isNullOrEmpty() && region.isNullOrEmpty()) {
|
if (endpoint.isNullOrEmpty() || region.isNullOrEmpty()) {
|
||||||
statusMessage = getString(R.string.screen_s3_settings_msg_endpoint_and_region_not_empty)
|
statusMessage = getString(R.string.screen_s3_settings_msg_endpoint_and_region_not_empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ package org.cryptomator.presentation.ui.fragment
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
|
||||||
import org.cryptomator.generator.Fragment
|
import org.cryptomator.generator.Fragment
|
||||||
import org.cryptomator.presentation.R
|
import org.cryptomator.presentation.R
|
||||||
import org.cryptomator.presentation.model.S3CloudModel
|
import org.cryptomator.presentation.model.S3CloudModel
|
||||||
@ -13,10 +12,9 @@ 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.bucketEditText
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_s3.createCloudButton
|
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.displayNameEditText
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_s3.regionOrEndpointEditText
|
import kotlinx.android.synthetic.main.fragment_setup_s3.endpointEditText
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_s3.regionOrEndpointEditTextLayout
|
import kotlinx.android.synthetic.main.fragment_setup_s3.regionEditText
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_s3.secretKeyEditText
|
import kotlinx.android.synthetic.main.fragment_setup_s3.secretKeyEditText
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_s3.toggleCustomS3
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@Fragment(R.layout.fragment_setup_s3)
|
@Fragment(R.layout.fragment_setup_s3)
|
||||||
@ -40,17 +38,6 @@ class S3AddOrChangeFragment : BaseFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showEditableCloudContent(s3CloudModel)
|
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?) {
|
private fun showEditableCloudContent(s3CloudModel: S3CloudModel?) {
|
||||||
@ -60,16 +47,9 @@ class S3AddOrChangeFragment : BaseFragment() {
|
|||||||
accessKeyEditText.setText(decrypt(s3CloudModel.accessKey()))
|
accessKeyEditText.setText(decrypt(s3CloudModel.accessKey()))
|
||||||
secretKeyEditText.setText(decrypt(s3CloudModel.secretKey()))
|
secretKeyEditText.setText(decrypt(s3CloudModel.secretKey()))
|
||||||
bucketEditText.setText(s3CloudModel.s3Bucket())
|
bucketEditText.setText(s3CloudModel.s3Bucket())
|
||||||
|
endpointEditText.setText(s3CloudModel.s3Endpoint())
|
||||||
if (it.s3Endpoint().isNotEmpty()) {
|
regionEditText.setText(s3CloudModel.s3Region())
|
||||||
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 {
|
private fun decrypt(text: String?): String {
|
||||||
@ -91,11 +71,14 @@ class S3AddOrChangeFragment : BaseFragment() {
|
|||||||
val bucket = bucketEditText.text.toString().trim()
|
val bucket = bucketEditText.text.toString().trim()
|
||||||
val displayName = displayNameEditText.text.toString().trim()
|
val displayName = displayNameEditText.text.toString().trim()
|
||||||
|
|
||||||
if (toggleCustomS3.isChecked) {
|
s3AddOrChangePresenter.checkUserInput( //
|
||||||
s3AddOrChangePresenter.checkUserInput(accessKey, secretKey, bucket, null, regionOrEndpointEditText.text.toString().trim(), cloudId, displayName)
|
accessKey, //
|
||||||
} else {
|
secretKey, //
|
||||||
s3AddOrChangePresenter.checkUserInput(accessKey, secretKey, bucket, regionOrEndpointEditText.text.toString().trim(), null, cloudId, displayName)
|
bucket, //
|
||||||
}
|
endpointEditText.text.toString().trim(), //
|
||||||
|
regionEditText.text.toString().trim(), //
|
||||||
|
cloudId, //
|
||||||
|
displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideKeyboard() {
|
fun hideKeyboard() {
|
||||||
|
@ -79,27 +79,34 @@
|
|||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/regionOrEndpointEditTextLayout"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/regionOrEndpointEditText"
|
android:id="@+id/endpointEditText"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/screen_s3_settings_endpoint_label"
|
||||||
android:imeOptions="flagNoPersonalizedLearning"
|
android:imeOptions="flagNoPersonalizedLearning"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:singleLine="true" />
|
android:singleLine="true" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/toggleCustomS3"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:checked="true"
|
android:id="@+id/regionEditText"
|
||||||
android:text="@string/screen_s3_settings_amazon_s3_text" />
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/screen_s3_settings_region_label"
|
||||||
|
android:imeOptions="flagNoPersonalizedLearning"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:singleLine="true" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/createCloudButton"
|
android:id="@+id/createCloudButton"
|
||||||
|
@ -193,7 +193,7 @@
|
|||||||
<string name="screen_s3_settings_msg_access_key_not_empty">Access Key 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_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_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>
|
<string name="screen_s3_settings_msg_endpoint_and_region_not_empty">Endpoint or Region can\'t be empty</string>
|
||||||
|
|
||||||
<!-- ## screen: enter vault name -->
|
<!-- ## 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_title" translatable="false">@string/screen_vault_list_action_create_new_vault</string>
|
||||||
|
@ -99,6 +99,13 @@
|
|||||||
android:action="android.intent.action.VIEW"
|
android:action="android.intent.action.VIEW"
|
||||||
android:data="https://github.com/microsoftgraph/msgraph-sdk-android" />
|
android:data="https://github.com/microsoftgraph/msgraph-sdk-android" />
|
||||||
</Preference>
|
</Preference>
|
||||||
|
<Preference
|
||||||
|
android:summary="Apache License v2"
|
||||||
|
android:title="MinIO Client SDK for Java">
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:data="https://github.com/minio/minio-java" />
|
||||||
|
</Preference>
|
||||||
<Preference
|
<Preference
|
||||||
android:summary="Apache License v2"
|
android:summary="Apache License v2"
|
||||||
android:title="OkHttp (Square, Inc.)">
|
android:title="OkHttp (Square, Inc.)">
|
||||||
@ -134,13 +141,6 @@
|
|||||||
android:action="android.intent.action.VIEW"
|
android:action="android.intent.action.VIEW"
|
||||||
android:data="https://github.com/ReactiveX/RxJava" />
|
android:data="https://github.com/ReactiveX/RxJava" />
|
||||||
</Preference>
|
</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
|
<Preference
|
||||||
android:summary="Apache License v2"
|
android:summary="Apache License v2"
|
||||||
android:title="Subsampling Scale Image View">
|
android:title="Subsampling Scale Image View">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user