Merge branch 'release/1.5.18'

This commit is contained in:
Julian Raufelder 2021-05-07 11:32:38 +02:00
commit fe3c408a53
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
74 changed files with 2097 additions and 112 deletions

View File

@ -42,7 +42,7 @@ allprojects {
ext {
androidApplicationId = 'org.cryptomator'
androidVersionCode = getVersionCode()
androidVersionName = '1.5.17'
androidVersionName = '1.5.18'
}
repositories {
mavenCentral()

View File

@ -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}",

View File

@ -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

View File

@ -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);

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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());
}
}
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View 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;
}
}

View File

@ -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);
}
}

View 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);
}
}
}

View 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();
}

View File

@ -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() {

View 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)
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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());

View File

@ -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);

View File

@ -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
}

View 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);
}
}
}

View File

@ -0,0 +1,11 @@
package org.cryptomator.domain.exception;
public class NoSuchBucketException extends BackendException {
public NoSuchBucketException() {
}
public NoSuchBucketException(String name) {
super(name);
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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"

View File

@ -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);
}

View File

@ -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())

View File

@ -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();
}

View File

@ -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) //

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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 = //

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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
}
}

View File

@ -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"

View File

@ -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) }
}

View File

@ -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
}
}
}

View File

@ -29,9 +29,10 @@ class SetPasswordFragment : BaseFragment() {
}
false
}
passwordStrengthUtil.startUpdatingPasswortStrengthMeter(passwordEditText, //
passwordStrengthUtil.startUpdatingPasswordStrengthMeter(passwordEditText, //
progressBarPwStrengthIndicator, //
textViewPwStrengthIndicator)
textViewPwStrengthIndicator, //
createVaultButton)
passwordEditText.requestFocus()
}

View File

@ -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)
}
}
}

View File

@ -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());
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 -->

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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

View File

@ -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()