diff --git a/app/build.gradle b/app/build.gradle index 1c3dfc29002beee49dade71c4ccd23083531c64a..9d45c7bad7312f02d3cf02d08b1165e99384f36c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,8 +4,8 @@ plugins { def versionMajor = 1 -def versionMinor = 1 -def versionPatch = 1 +def versionMinor = 2 +def versionPatch = 0 diff --git a/app/src/main/java/foundation/e/drive/models/SyncWrapper.java b/app/src/main/java/foundation/e/drive/models/SyncWrapper.java index f6bc93133ed9ee74bcd0d5de499b9d4ae60c676b..a1984972596c3eff221fd25ab9350f9afb7a68d7 100644 --- a/app/src/main/java/foundation/e/drive/models/SyncWrapper.java +++ b/app/src/main/java/foundation/e/drive/models/SyncWrapper.java @@ -9,9 +9,12 @@ package foundation.e.drive.models; import android.accounts.Account; import android.content.Context; +import android.util.Log; import com.owncloud.android.lib.common.operations.RemoteOperation; +import java.io.File; + import foundation.e.drive.operations.DownloadFileOperation; import foundation.e.drive.operations.UploadFileOperation; @@ -21,6 +24,7 @@ import foundation.e.drive.operations.UploadFileOperation; * @author Vincent Bourgmayer */ public class SyncWrapper { + private final static String TAG = SyncWrapper.class.getSimpleName(); private final SyncRequest request; private final RemoteOperation remoteOperation; private boolean isRunning; @@ -59,6 +63,12 @@ public class SyncWrapper { switch (request.getOperationType()) { case UPLOAD: final SyncedFileState sfs = request.getSyncedFileState(); + final File file = new File(sfs.getLocalPath()); + if (!file.exists()) { + operation = null; + Log.w(TAG, "createRemoteOperation: local file doesn't exist for upload"); + break; + } operation = new UploadFileOperation(sfs, account, context); break; case DOWNLOAD: diff --git a/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java b/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java index 4cfc10b41ff698d3985a4807a44225304953ea7e..2c6bdb0971a044bcdf644e2be0c4453b20b092cc 100644 --- a/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java @@ -20,12 +20,17 @@ import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.UserInfo; import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.resources.files.ChunkedFileUploadRemoteOperation; import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation; import com.owncloud.android.lib.resources.files.FileUtils; +import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation; import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation; +import com.owncloud.android.lib.resources.files.model.RemoteFile; import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; import java.io.File; +import java.util.ArrayList; + import foundation.e.drive.database.DbHelper; import foundation.e.drive.models.SyncedFileState; import foundation.e.drive.utils.CommonUtils; @@ -37,12 +42,10 @@ import foundation.e.drive.utils.DavClientProvider; */ public class UploadFileOperation extends RemoteOperation { private final static String TAG = UploadFileOperation.class.getSimpleName(); - - private int restartCounter =0; - private long previousLastModified; //get to restore real value if all trials fails + private final static int FILE_SIZE_FLOOR_FOR_CHUNKED = 3072000; //3MB private final Context context; private final SyncedFileState syncedState; - private final Account account; // /!\ this is temporary because NC library doesn't use NextcloudClient for every operation yet + private final Account account; // TODO Remove as soon as nextcloud library move all Operation to NextcloudClient instead of OwncloudClient /** * Construct an upload operation with an already known syncedFileState @@ -50,7 +53,6 @@ public class UploadFileOperation extends RemoteOperation { */ public UploadFileOperation (final SyncedFileState syncedFileState, final Account account, final Context context) { this.syncedState = syncedFileState; - this.previousLastModified = syncedState.getLocalLastModified(); this.context = context; this.account = account; } @@ -69,68 +71,111 @@ public class UploadFileOperation extends RemoteOperation { @Override protected RemoteOperationResult run(OwnCloudClient client ) { final File file = new File(syncedState.getLocalPath()); + + final ResultCode conditionCheckResult = checkCondition(file, client); + if (conditionCheckResult != ResultCode.OK) { + return new RemoteOperationResult(conditionCheckResult); + } + + final RemoteOperationResult uploadResult; + if (file.length() >= FILE_SIZE_FLOOR_FOR_CHUNKED) { + Log.d(TAG, "upload " + file.getName() + " as chunked file"); + uploadResult = uploadChunkedFile(file, client); + } else { + uploadResult = uploadFile(file, client); + } + + final ResultCode resultCode; + if (uploadResult.isSuccess()) { + updateSyncedFileState(uploadResult, file.lastModified(), client); + resultCode = uploadResult.getCode(); + } else { + resultCode = onUploadFailure(uploadResult.getCode(), file.getName()); + } + + DbHelper.manageSyncedFileStateDB(syncedState, "UPDATE", context); + return new RemoteOperationResult(resultCode); + } + + + /** + * Handle upload's failure + * @param uploadResult + * @param fileName + * @return + */ + private ResultCode onUploadFailure(final ResultCode uploadResult, final String fileName) { + if (uploadResult != ResultCode.CONFLICT + && uploadResult != ResultCode.QUOTA_EXCEEDED) { + Log.e(TAG, "UploadFileRemoteOperation for : " + fileName + " failed => code: " + uploadResult); + return ResultCode.UNKNOWN_ERROR; + } + return uploadResult; + } + + /** + * Check condition required to upload the file: + * - the local file exist + * - the local file is not already up to date with the remote one + * - there is enough free storage + * - the remote directory exists + * @param file file to upload + * @param client client used to execute some request + * @return ResultCode.OK if everything is alright + */ + private ResultCode checkCondition(final File file, final OwnCloudClient client) { if (file == null || !file.exists()) { Log.w(TAG, "Can't get the file. It might have been deleted"); - return new RemoteOperationResult(ResultCode.FORBIDDEN); + return ResultCode.FORBIDDEN; } - final String targetPath = syncedState.getRemotePath(); - //If file already up-to-date & synced if (syncedState.isLastEtagStored() && syncedState.getLocalLastModified() == file.lastModified()) { Log.d(TAG, "syncedState last modified: "+ syncedState.getLocalLastModified()+" <=> file last modified: "+file.lastModified() +": So return sync_conflict"); - return new RemoteOperationResult(ResultCode.SYNC_CONFLICT); + return ResultCode.SYNC_CONFLICT; } + final NextcloudClient ncClient = DavClientProvider.getInstance().getNcClientInstance(account, context); final RemoteOperationResult checkQuotaResult = checkAvailableSpace(ncClient, file.length()); if (checkQuotaResult.getCode() != ResultCode.OK) { Log.e(TAG, "Impossible to check quota. Upload of " + syncedState.getLocalPath() + "cancelled"); - return new RemoteOperationResult(checkQuotaResult.getCode()); + return checkQuotaResult.getCode(); } + final String targetPath = syncedState.getRemotePath(); if (!createRemoteFolder(targetPath, client)) { - return new RemoteOperationResult(ResultCode.UNKNOWN_ERROR); + return ResultCode.UNKNOWN_ERROR; } + return ResultCode.OK; + } - final ResultCode resultCode; - boolean mustRestart = true; - final RemoteOperationResult uploadResult = uploadFile(file, client); - if (uploadResult.isSuccess()) { - final String etag = uploadResult.getResultData(); - if (etag != null) { - syncedState.setLastETAG(etag); - } - syncedState.setLocalLastModified(file.lastModified()); - resultCode = uploadResult.getCode(); - mustRestart = false; - } else { - if (uploadResult.getCode() == ResultCode.CONFLICT ) { - resultCode = ResultCode.CONFLICT; - Log.d(TAG, "Catched a conflict result for : "+ file.getName()); - mustRestart = false; - } else if (uploadResult.getCode() == ResultCode.QUOTA_EXCEEDED) { - resultCode = ResultCode.QUOTA_EXCEEDED; - mustRestart = false; - } else { - Log.e(TAG, "UploadFileRemoteOperation for : " + file.getName() + " failed => code: " + uploadResult.getCode()); - resultCode = ResultCode.UNKNOWN_ERROR; - mustRestart = false; + /** + * Update syncedFileState (etag & last modified) in case of successful upload + * @param uploadResult The Upload's result instance + * @param fileLastModified value of local file's last modified + * @param client The client used to check etag if missing + */ + private void updateSyncedFileState(final RemoteOperationResult uploadResult, final long fileLastModified, final OwnCloudClient client) { + //The below if statement should only be called for chunked upload. But + //for some unknown reason, the simple file upload doesn't give the etag in the result + // so, I moved the code here as a security + if (uploadResult.getResultData() == null) { + final RemoteOperationResult result = readRemoteFile(syncedState.getRemotePath(), client); + final ArrayList resultData = result.getData(); + if (result.isSuccess() && resultData != null && !resultData.isEmpty()) { + final String latestETag = ((RemoteFile) resultData.get(0)).getEtag(); + uploadResult.setResultData(latestETag); } } + final String etag = uploadResult.getResultData(); - if (mustRestart) { - if (this.restartCounter < 1) { - this.restartCounter += 1; - return this.run(client); - } else { - syncedState.setLocalLastModified(this.previousLastModified); //Revert syncFileState to its previous state - } + if (etag != null) { + syncedState.setLastETAG(etag); } - DbHelper.manageSyncedFileStateDB(syncedState, "UPDATE", context); - return new RemoteOperationResult(resultCode); + syncedState.setLocalLastModified(fileLastModified); } /** @@ -164,11 +209,41 @@ public class UploadFileOperation extends RemoteOperation { } /** - * Effectively upload the file + * Read remote file after upload to retrieve eTag + * @param remotePath file's remote path + * @param client Owncloudclient instance. @TODO will be replaced by NextcloudClient in future. + * @return RemoteOperationResult instance containing failure details or RemoteFile instance + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public RemoteOperationResult readRemoteFile(final String remotePath, final OwnCloudClient client) { + final ReadFileRemoteOperation readRemoteFile = new ReadFileRemoteOperation(remotePath); + return readRemoteFile.execute(client); + } + + /** + * Upload a chunked file + * Used for file bigger than 3MB + * @param file File to upload + * @param client OwncloudClient to perform the upload. @TODO will be replaced by NextcloudClient in future. + * @return RemoteOperationResult instance containing success or failure status with details + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public RemoteOperationResult uploadChunkedFile(final File file, final OwnCloudClient client) { + final String mimeType = CommonUtils.getMimeType(file); + final ChunkedFileUploadRemoteOperation uploadOperation = new ChunkedFileUploadRemoteOperation(syncedState.getLocalPath(), + syncedState.getRemotePath(), + mimeType, syncedState.getLastETAG(), + syncedState.getLocalLastModified()+"", false); + return uploadOperation.execute(client); + } + + + /** + * Upload a file * note: this has been extracted from run(...) for * testing purpose - * @param client client to run the method - * @return RemoteOperationResult + * @param client client to run the method. @TODO will be replaced by NextcloudClient in future. + * @return RemoteOperationResult the instance must contains etag in resultData if successful. */ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) public RemoteOperationResult uploadFile(final File file, final OwnCloudClient client) { @@ -183,10 +258,11 @@ public class UploadFileOperation extends RemoteOperation { /** * Create remote parent folder of the file if missing - * @param targetPath - * @param client still OwnCloudClient at the moment, but will be Nextcloud client in the futur - * @return + * @param targetPath Path of remote directory to create or check for existence + * @param client Client to perform the request. @TODO will be replaced by NextcloudClient in future. + * @return true if the parent directory has been created, false either */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) public boolean createRemoteFolder(String targetPath, OwnCloudClient client) { final String remoteFolderPath = targetPath.substring(0, targetPath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1); final CreateFolderRemoteOperation createFolderOperation = new CreateFolderRemoteOperation(remoteFolderPath, true); diff --git a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java index 020429da75d5b01dc65db3eb99f0d404c47c0c43..363e81672d2b8c2fe34ed883b4db4c085a8f3759 100644 --- a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java +++ b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java @@ -54,7 +54,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation private ConcurrentHashMap startedSync; //Integer is thread index (1 to workerAmount) private Account account; - private final int workerAmount = 4; + private final int workerAmount = 2; private Thread[] threadPool; @Deprecated private OwnCloudClient ocClient;