feat: add pCloud implementation

This commit is contained in:
Manuel Jenny 2021-03-12 17:15:03 +01:00
parent bb4572b9d0
commit 80dce5ec60
No known key found for this signature in database
GPG Key ID: 1C80FE62B2BEAA18
5 changed files with 545 additions and 1 deletions

View File

@ -0,0 +1,37 @@
package org.cryptomator.data.cloud.pcloud;
public enum PCloudApiErrorCodes {
LOGIN_REQUIRED(1000),
NO_FULL_PATH_OR_NAME_FOLDER_ID_PROVIDED(1001),
NO_FULL_PATH_OR_FOLDER_ID_PROVIDED(1002),
NO_DESTINATION_PROVIDED(1016),
INVALID_FOLDER_ID(1017),
INVALID_DESTINATION(1037),
PROVIDE_URL(1040),
LOGIN_FAILED(2000),
INVALID_FILE_FOLDER_NAME(2001),
COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST(2002),
ACCESS_DENIED(2003),
FILE_OR_FOLDER_ALREADY_EXISTS(2004),
DIRECTORY_DOES_NOT_EXIST(2005),
FOLDER_NOT_EMPTY(2006),
CANNOT_DELETE_ROOT_FOLDER(2007),
USER_OVER_QUOTA(2008),
FILE_NOT_FOUND(2009),
SHARED_FOLDER_IN_SHARED_FOLDER(2023),
ACTIVE_SHARES_OR_SHAREREQUESTS_PRESENT(2028),
CANNOT_RENAME_ROOT_FOLDER(2042),
CANNOT_MOVE_FOLDER_INTO_SUBFOLDER_OF_ITSELF(2043),
INVALID_ACCESS_TOKEN(2094),
TOO_MANY_LOGIN_TRIES_FROM_IP(4000),
INTERNAL_ERROR(5000),
INTERNAL_UPLOAD_ERROR(5001);
private final int value;
PCloudApiErrorCodes(final int newValue) {
value = newValue;
}
public int getValue() { return value; }
}

View File

@ -0,0 +1,54 @@
package org.cryptomator.data.cloud.pcloud;
import android.content.Context;
import com.pcloud.sdk.ApiClient;
import com.pcloud.sdk.Authenticators;
import com.pcloud.sdk.PCloudSdk;
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor;
import org.cryptomator.util.SharedPreferencesHandler;
import org.cryptomator.util.file.LruFileCacheUtil;
import okhttp3.Cache;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import timber.log.Timber;
import static org.cryptomator.data.util.NetworkTimeout.CONNECTION;
import static org.cryptomator.data.util.NetworkTimeout.READ;
import static org.cryptomator.data.util.NetworkTimeout.WRITE;
class PCloudClientFactory {
private ApiClient apiClient;
private static Interceptor httpLoggingInterceptor(Context context) {
return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context);
}
public ApiClient getClient(String accessToken, String url, Context context) {
if (apiClient == null) {
final SharedPreferencesHandler sharedPreferencesHandler = new SharedPreferencesHandler(context);
apiClient = createApiClient(accessToken, url, context, sharedPreferencesHandler.useLruCache(), sharedPreferencesHandler.lruCacheSize());
}
return apiClient;
}
private ApiClient createApiClient(String accessToken, String url, Context context, boolean useLruCache, int lruCacheSize) {
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient() //
.newBuilder() //
.connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) //
.readTimeout(READ.getTimeout(), READ.getUnit()) //
.writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) //
.addInterceptor(httpLoggingInterceptor(context)); //;
if (useLruCache) {
okHttpClientBuilder.cache(new Cache(new LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.PCLOUD), lruCacheSize));
}
OkHttpClient okHttpClient = okHttpClientBuilder.build();
return PCloudSdk.newClientBuilder().authenticator(Authenticators.newOAuthAuthenticator(accessToken)).withClient(okHttpClient).apiHost(url).create();
}
}

View File

@ -0,0 +1,209 @@
package org.cryptomator.data.cloud.pcloud;
import android.content.Context;
import com.pcloud.sdk.ApiError;
import org.cryptomator.data.cloud.InterceptingCloudContentRepository;
import org.cryptomator.domain.PCloudCloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.NetworkConnectionException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import static org.cryptomator.util.ExceptionUtil.contains;
class PCloudCloudContentRepository extends InterceptingCloudContentRepository<PCloudCloud, PCloudNode, PCloudFolder, PCloudFile> {
private final PCloudCloud cloud;
public PCloudCloudContentRepository(PCloudCloud cloud, Context context) {
super(new Intercepted(cloud, context));
this.cloud = cloud;
}
@Override
protected void throwWrappedIfRequired(Exception e) throws BackendException {
throwConnectionErrorIfRequired(e);
throwWrongCredentialsExceptionIfRequired(e);
}
private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException {
if (contains(e, IOException.class)) {
throw new NetworkConnectionException(e);
}
}
private void throwWrongCredentialsExceptionIfRequired(Exception e) {
if (e instanceof ApiError && ((ApiError) e).errorCode() == PCloudApiErrorCodes.INVALID_ACCESS_TOKEN.getValue()) {
throw new WrongCredentialsException(cloud);
}
}
private static class Intercepted implements CloudContentRepository<PCloudCloud, PCloudNode, PCloudFolder, PCloudFile> {
private final PCloudImpl cloud;
public Intercepted(PCloudCloud cloud, Context context) {
this.cloud = new PCloudImpl(cloud, context);
}
public PCloudFolder root(PCloudCloud cloud) {
return this.cloud.root();
}
@Override
public PCloudFolder resolve(PCloudCloud cloud, String path) {
return this.cloud.resolve(path);
}
@Override
public PCloudFile file(PCloudFolder parent, String name) {
return cloud.file(parent, name);
}
@Override
public PCloudFile file(PCloudFolder parent, String name, Optional<Long> size) throws BackendException {
return cloud.file(parent, name, size);
}
@Override
public PCloudFolder folder(PCloudFolder parent, String name) {
return cloud.folder(parent, name);
}
@Override
public boolean exists(PCloudNode node) throws BackendException {
try {
return cloud.exists(node);
} catch (ApiError|IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public List<PCloudNode> list(PCloudFolder folder) throws BackendException {
try {
return cloud.list(folder);
} catch (ApiError | IOException e) {
if (e instanceof ApiError) {
if (((ApiError) e).errorCode() == PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.getValue()) {
throw new NoSuchCloudFileException();
}
}
throw new FatalBackendException(e);
}
}
@Override
public PCloudFolder create(PCloudFolder folder) throws BackendException {
try {
return cloud.create(folder);
} catch (ApiError | IOException e) {
if (e instanceof ApiError) {
if (((ApiError) e).errorCode() == PCloudApiErrorCodes.FILE_OR_FOLDER_ALREADY_EXISTS.getValue())
throw new CloudNodeAlreadyExistsException(folder.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public PCloudFolder move(PCloudFolder source, PCloudFolder target) throws BackendException {
try {
return (PCloudFolder) cloud.move(source, target);
} catch (ApiError | IOException e) {
if (e instanceof ApiError) {
if (((ApiError)e).errorCode() == PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.getValue()) {
throw new NoSuchCloudFileException(source.getName());
} else if (((ApiError)e).errorCode() == PCloudApiErrorCodes.FILE_OR_FOLDER_ALREADY_EXISTS.getValue()) {
throw new CloudNodeAlreadyExistsException(target.getName());
}
throw new CloudNodeAlreadyExistsException(target.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public PCloudFile move(PCloudFile source, PCloudFile target) throws BackendException {
try {
return (PCloudFile) cloud.move(source, target);
} catch (ApiError | IOException e) {
if (e instanceof ApiError) {
if (((ApiError)e).errorCode() == PCloudApiErrorCodes.FILE_NOT_FOUND.getValue()) {
throw new NoSuchCloudFileException(source.getName());
} else if (((ApiError)e).errorCode() == PCloudApiErrorCodes.FILE_OR_FOLDER_ALREADY_EXISTS.getValue()) {
throw new CloudNodeAlreadyExistsException(target.getName());
}
throw new CloudNodeAlreadyExistsException(target.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public PCloudFile write(PCloudFile uploadFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
try {
return cloud.write(uploadFile, data, progressAware, replace, size);
} catch (ApiError | IOException e) {
if (((ApiError)e).errorCode() == PCloudApiErrorCodes.FILE_NOT_FOUND.getValue()) {
throw new NoSuchCloudFileException(uploadFile.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public void read(PCloudFile file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
try {
cloud.read(file, data, progressAware);
} catch (ApiError | IOException e) {
if (((ApiError)e).errorCode() == PCloudApiErrorCodes.FILE_NOT_FOUND.getValue()) {
throw new NoSuchCloudFileException(file.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public void delete(PCloudNode node) throws BackendException {
try {
cloud.delete(node);
} catch (ApiError | IOException e) {
if (((ApiError)e).errorCode() == PCloudApiErrorCodes.FILE_NOT_FOUND.getValue()) {
throw new NoSuchCloudFileException(node.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public String checkAuthenticationAndRetrieveCurrentAccount(PCloudCloud cloud) throws BackendException {
try {
return this.cloud.currentAccount();
} catch (ApiError | IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public void logout(PCloudCloud cloud) throws BackendException {
// empty
}
}
}

View File

@ -0,0 +1,243 @@
package org.cryptomator.data.cloud.pcloud;
import android.content.Context;
import com.pcloud.sdk.ApiClient;
import com.pcloud.sdk.ApiError;
import com.pcloud.sdk.DataSink;
import com.pcloud.sdk.DownloadOptions;
import com.pcloud.sdk.FileLink;
import com.pcloud.sdk.ProgressListener;
import com.pcloud.sdk.RemoteEntry;
import com.pcloud.sdk.RemoteFile;
import com.pcloud.sdk.RemoteFolder;
import com.pcloud.sdk.UploadOptions;
import com.pcloud.sdk.UserInfo;
import org.cryptomator.data.util.CopyStream;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.PCloudCloud;
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.Progress;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import org.cryptomator.util.crypto.CredentialCryptor;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;
import okio.Source;
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
class PCloudImpl {
private static final int DIRECTORY_DOES_NOT_EXIST = 2005;
private static final int INVALID_FILE_OR_FOLDER_NAME = 2001;
private final PCloudClientFactory clientFactory = new PCloudClientFactory();
private final PCloudCloud cloud;
private final RootPCloudFolder root;
private final Context context;
PCloudImpl(PCloudCloud cloud, Context context) {
if (cloud.accessToken() == null) {
throw new NoAuthenticationProvidedException(cloud);
}
this.cloud = cloud;
this.root = new RootPCloudFolder(cloud);
this.context = context;
}
private ApiClient client() {
return clientFactory.getClient(decrypt(cloud.accessToken()), cloud.url(), context);
}
private String decrypt(String password) {
return CredentialCryptor //
.getInstance(context) //
.decrypt(password);
}
public PCloudFolder root() {
return root;
}
public PCloudFolder resolve(String path) {
if (path.startsWith("/")) {
path = path.substring(1);
}
String[] names = path.split("/");
PCloudFolder folder = root;
for (String name : names) {
folder = folder(folder, name);
}
return folder;
}
public PCloudFile file(CloudFolder folder, String name) {
return file(folder, name, Optional.empty());
}
public PCloudFile file(CloudFolder folder, String name, Optional<Long> size) {
return PCloudCloudNodeFactory.file( //
(PCloudFolder) folder, //
name, //
size, //
folder.getPath() + '/' + name);
}
public PCloudFolder folder(CloudFolder folder, String name) {
return PCloudCloudNodeFactory.folder( //
(PCloudFolder) folder, //
name, //
folder.getPath() + '/' + name);
}
public boolean exists(CloudNode node) throws ApiError, IOException {
try {
if (node instanceof PCloudFolder) {
client().listFolder(((PCloudFolder) node).getPath()).execute();
return true;
} else {
client().stat(((PCloudFile)node).getPath()).execute();
return true;
}
} catch (ApiError e) {
if (e.errorCode() == DIRECTORY_DOES_NOT_EXIST || e.errorCode() == INVALID_FILE_OR_FOLDER_NAME) {
return false;
}
throw e;
}
}
public List<PCloudNode> list(CloudFolder folder) throws ApiError, IOException {
List<PCloudNode> result = new ArrayList<>();
RemoteFolder listFolderResult = null;
List<RemoteEntry> entryMetadata = listFolderResult.children();
for (RemoteEntry metadata : entryMetadata) {
result.add(PCloudCloudNodeFactory.from( //
(PCloudFolder) folder, //
metadata));
}
return result;
}
public PCloudFolder create(CloudFolder folder) throws ApiError, IOException {
RemoteFolder createFolderResult = client() //
.createFolder(((PCloudFolder)folder.getParent()).getId(), folder.getName()) //
.execute();
return PCloudCloudNodeFactory.from( //
(PCloudFolder) folder.getParent(), //
createFolderResult.asFolder());
}
public CloudNode move(CloudNode source, CloudNode target) throws ApiError, IOException {
RemoteEntry relocationResult;
if (source instanceof PCloudFolder) {
relocationResult = client().moveFolder(((PCloudFolder) source).getId(), ((PCloudFolder) target).getId()).execute();
} else {
relocationResult = client().moveFile(((PCloudFile) source).getId(), ((PCloudFolder) target).getId()).execute();
}
return PCloudCloudNodeFactory.from( //
(PCloudFolder) target.getParent(), //
relocationResult);
}
public PCloudFile write(PCloudFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, long size) throws ApiError, IOException, CloudNodeAlreadyExistsException {
if (exists(file) && !replace) {
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
}
progressAware.onProgress(Progress.started(UploadState.upload(file)));
UploadOptions uploadOptions = UploadOptions.DEFAULT;
if (replace) {
uploadOptions = UploadOptions.OVERRIDE_FILE;
}
RemoteFile uploadedFile = uploadFile(file, data, progressAware, uploadOptions, size);
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
return PCloudCloudNodeFactory.from( //
file.getParent(), //
uploadedFile);
}
private RemoteFile uploadFile(final PCloudFile file, DataSource data, final ProgressAware<UploadState> progressAware, UploadOptions uploadOptions, final long size) //
throws ApiError, IOException {
ProgressListener listener = (done, total) -> progressAware.onProgress( //
progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(done));
com.pcloud.sdk.DataSource pCloudDataSource = new com.pcloud.sdk.DataSource() {
@Override
public void writeTo(BufferedSink sink) throws IOException {
try (Source source = Okio.source(data.open(context))) {
sink.writeAll(source);
}
}
};
return client() //
.createFile(((PCloudFolder) file.getParent()).getId(), file.getName(), pCloudDataSource, new Date(), listener, uploadOptions) //
.execute();
}
public void read(CloudFile file, OutputStream data, final ProgressAware<DownloadState> progressAware) throws ApiError, IOException {
progressAware.onProgress(Progress.started(DownloadState.download(file)));
FileLink fileLink = client().createFileLink(((PCloudFile) file).getId(), DownloadOptions.DEFAULT).execute();
ProgressListener listener = (done, total) -> progressAware.onProgress( //
progress(DownloadState.download(file)) //
.between(0) //
.and(file.getSize().orElse(Long.MAX_VALUE)) //
.withValue(done));
DataSink sink = new DataSink() {
@Override
public void readAll(BufferedSource source) throws IOException {
CopyStream.copyStreamToStream(source.getBuffer().inputStream(), data);
}
};
client().download(fileLink, sink, listener).execute();
progressAware.onProgress(Progress.completed(DownloadState.download(file)));
}
public void delete(CloudNode node) throws ApiError, IOException {
if (node instanceof PCloudFolder) {
client() //
.deleteFolder(((PCloudFolder) node).getId()).execute();
} else {
client() //
.deleteFile(((PCloudFile) node).getId()).execute();
}
}
public String currentAccount() throws ApiError, IOException {
UserInfo currentAccount = client() //
.getUserInfo() //
.execute();
return currentAccount.email();
}
}

View File

@ -21,13 +21,14 @@ class LruFileCacheUtil(context: Context) {
private val parent: File = context.cacheDir
enum class Cache {
DROPBOX, WEBDAV, ONEDRIVE, GOOGLE_DRIVE
DROPBOX, WEBDAV, PCLOUD, ONEDRIVE, GOOGLE_DRIVE
}
fun resolve(cache: Cache?): File {
return when (cache) {
Cache.DROPBOX -> File(parent, "LruCacheDropbox")
Cache.WEBDAV -> File(parent, "LruCacheWebdav")
Cache.PCLOUD -> File(parent, "LruCachePCloud")
Cache.ONEDRIVE -> File(parent, "LruCacheOneDrive")
Cache.GOOGLE_DRIVE -> File(parent, "LruCacheGoogleDrive")
else -> throw IllegalStateException()