Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cd7c3e9c authored by Vincent Bourgmayer's avatar Vincent Bourgmayer
Browse files

Merge branch '284-o-chunkUpload-v2' into 'v1-oreo'

284 o chunk upload v2

Closes backlog#284

See merge request !147
parents 47e09b9d 7be9539b
Loading
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -4,8 +4,8 @@ plugins {


def versionMajor = 1
def versionMinor = 1
def versionPatch = 1
def versionMinor = 2
def versionPatch = 0



+10 −0
Original line number Diff line number Diff line
@@ -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:
+125 −49
Original line number Diff line number Diff line
@@ -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<String> 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<String> 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<String> 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<Object> 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<String> 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);
+1 −1
Original line number Diff line number Diff line
@@ -54,7 +54,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation
    private ConcurrentHashMap<Integer, SyncWrapper> 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;