diff --git a/app/src/main/java/foundation/e/drive/operations/UploadChunkedFileOperation.java b/app/src/main/java/foundation/e/drive/operations/UploadChunkedFileOperation.java new file mode 100644 index 0000000000000000000000000000000000000000..dee5655e67830bbb6bf01e322e5f04dd40ec9d2f --- /dev/null +++ b/app/src/main/java/foundation/e/drive/operations/UploadChunkedFileOperation.java @@ -0,0 +1,192 @@ +package foundation.e.drive.operations; + +import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.ETAG_CHANGED; +import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.FILE_NOT_FOUND; + +import android.content.Context; +import android.util.Log; + +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.CheckEtagRemoteOperation; +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.MoveFileRemoteOperation; +import com.owncloud.android.lib.resources.users.GetRemoteUserInfoOperation; + +import java.io.File; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.utils.CommonUtils; + +public class UploadChunkedFileOperation extends RemoteOperation { + private static final String TAG = UploadChunkedFileOperation.class.getSimpleName(); + private final SyncedFileState syncedFileState; + private final Context context; + + public UploadChunkedFileOperation(SyncedFileState syncedFileState, Context context){ + super(); + this.syncedFileState = syncedFileState; + this.context = context; + } + + @Override + protected RemoteOperationResult run(OwnCloudClient client) { + Log.d(TAG, "Start chunk upload for: " + syncedFileState.getLocalPath()); + final File file = new File(syncedFileState.getLocalPath()); + final long fileSize = new File(syncedFileState.getLocalPath()).length(); + if (fileSize == 0L) { + Log.w(TAG, "Upload aborted: " + syncedFileState.getLocalPath() + " doesn't exist"); + return new RemoteOperationResult(FILE_NOT_FOUND); + } + + final RemoteOperationResult.ResultCode quotaCheckResultCode = checkQuota(fileSize, client); + if(!quotaCheckResultCode.equals(RemoteOperationResult.ResultCode.OK)) { + return new RemoteOperationResult(quotaCheckResultCode); + } + Log.d(TAG, "Quota ok for chunked upload"); + + final String tempDirRemotePath = "test-chunked-upload-" + syncedFileState.getName(); + final RemoteOperationResult.ResultCode mkcolTempDirResultCode = createRemoteFolder(tempDirRemotePath, client); + if(!mkcolTempDirResultCode.equals(RemoteOperationResult.ResultCode.OK)){ + return new RemoteOperationResult(mkcolTempDirResultCode); + } + + Log.d(TAG, "Temp remote folder created"); + final RemoteOperationResult.ResultCode uploadResultCode = uploadChunks(tempDirRemotePath, CommonUtils.getMimeType(file), client); + if(!uploadResultCode.equals(RemoteOperationResult.ResultCode.OK)){ + //@TODO check different situation + return new RemoteOperationResult(uploadResultCode); + } + + Log.d(TAG, "Chunks uploaded"); + final String finalDirRemotePath = syncedFileState.getRemotePath().substring(0, syncedFileState.getRemotePath().lastIndexOf(FileUtils.PATH_SEPARATOR) + 1); + final RemoteOperationResult.ResultCode mkcolTargetDirResultCode = createRemoteFolder(finalDirRemotePath, client); + if(!mkcolTargetDirResultCode.equals(RemoteOperationResult.ResultCode.OK)){ + return new RemoteOperationResult(mkcolTargetDirResultCode); + } + Log.d(TAG, "Target dir exists"); + + final RemoteOperationResult.ResultCode assembleFileResultCode= assembleRemoteFile(tempDirRemotePath, syncedFileState.getRemotePath(), client); + if(!assembleFileResultCode.equals(RemoteOperationResult.ResultCode.OK)) { + return new RemoteOperationResult(assembleFileResultCode); + } + Log.d(TAG, "File assembled"); + final String eTag = getEtag(syncedFileState.getRemotePath(), client); + if(eTag.isEmpty()) { + //@TODO: invalid eTag + } + Log.d(TAG, "Etag obtained: "+eTag); + syncedFileState.setLastETAG(eTag); + DbHelper.manageSyncedFileStateDB(syncedFileState, "UPDATE", context); + + + return new RemoteOperationResult(RemoteOperationResult.ResultCode.OK); + } + + + /** + * Check if there is enough quota on ecloud for the file to upload + * @param client + * @return OK || UNKNOWN_ERROR || QUOTA_EXCEEDED + */ + private RemoteOperationResult.ResultCode checkQuota(long fileSize, OwnCloudClient client) { + final GetRemoteUserInfoOperation getRemoteUserInfoOperation = new GetRemoteUserInfoOperation(); + final RemoteOperationResult ocsResult = executeOperation(getRemoteUserInfoOperation, client); + + if (ocsResult.isSuccess() && ocsResult.getData() != null) { + final UserInfo userInfo = (UserInfo) ocsResult.getData().get(0); + final long availableQuota = userInfo.getQuota().getFree(); + if (availableQuota < fileSize ) { + Log.w(TAG, "quota exceeded!"); + return RemoteOperationResult.ResultCode.QUOTA_EXCEEDED; + } + } else { + Log.w(TAG, "getRemoteUserInfoOperation failed: " + ocsResult.getHttpCode()); + } + return ocsResult.getCode(); + } + + + /** + * Create a remote folder (can be final folder or temporary folder) + * @param path remote path of the folder + * @param client + * @return + */ + private RemoteOperationResult.ResultCode createRemoteFolder(String path, OwnCloudClient client) { + final CreateFolderRemoteOperation createFolderOperation = new CreateFolderRemoteOperation(path, true); + final RemoteOperationResult createFolderResult = executeOperation(createFolderOperation, client); + + if (!createFolderResult.isSuccess() && createFolderResult.getCode() != RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS) { + return createFolderResult.getCode(); + } + + return RemoteOperationResult.ResultCode.OK; + } + + /** + * Perform upload of file chunk by chunk + * @param client + * @return + */ + private RemoteOperationResult.ResultCode uploadChunks(String tempDirRemotePath, String mimeType, OwnCloudClient client) { + final ChunkedFileUploadRemoteOperation uploadsOperation = new ChunkedFileUploadRemoteOperation(context, syncedFileState.getLocalPath(), tempDirRemotePath, mimeType, syncedFileState.getLastETAG(), syncedFileState.getLocalLastModified()+"" ); + final RemoteOperationResult uploadsResult = executeOperation(uploadsOperation, client); + if(!uploadsResult.isSuccess()){ + Log.w(TAG, "upload is not successfull"); + } + //@TODO handle lot of failure case + return uploadsResult.getCode(); + } + + /** + * Assemble the remote chunks + * @param tempDirPath + * @param targetPath + * @param client + * @return + */ + private RemoteOperationResult.ResultCode assembleRemoteFile(String tempDirPath, String targetPath, OwnCloudClient client) { + final MoveFileRemoteOperation moveFileRemoteOperation = new MoveFileRemoteOperation(tempDirPath+"/.files", targetPath, true); + final RemoteOperationResult moveFileResult = executeOperation(moveFileRemoteOperation, client); + + if(!moveFileResult.isSuccess()) { + Log.e(TAG, "Assemble remote chunks failed"); + } + //@TODO handle lot of failure case + return RemoteOperationResult.ResultCode.OK; + } + + private String getEtag(String filepath, OwnCloudClient client) { + final CheckEtagRemoteOperation checkEtagOperation = new CheckEtagRemoteOperation(filepath, ""); + final RemoteOperationResult checkEtagResult = executeOperation(checkEtagOperation, client); + + if (checkEtagResult.getCode().equals(ETAG_CHANGED) && checkEtagResult.getData() != null) { + return (String) checkEtagResult.getData().get(0); + } + + return ""; + } + + + /** + * Method to catch SSL_ERROR or timeout socket connection, etc. + * @param operation + * @param client + * @return + */ + private RemoteOperationResult executeOperation(RemoteOperation operation, OwnCloudClient client) { + try { + return operation.execute(client); + }catch (Exception e){ + Log.e(TAG, e.toString()); + return new RemoteOperationResult(RemoteOperationResult.ResultCode.UNKNOWN_ERROR); + } + } + +} 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 5b4c24ae50d72f4e268accd8271cb4b476004b8e..9c4fcc11f2c49a28c1adc68f374d21efeb8a330d 100644 --- a/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java @@ -180,7 +180,7 @@ public class UploadFileOperation extends RemoteOperation { if (ocsResult.isSuccess() && ocsResult.getData() != null) { UserInfo userInfo = (UserInfo) ocsResult.getData().get(0); this.availableQuota = userInfo.getQuota().getFree(); - if (((UserInfo) ocsResult.getData().get(0)).getQuota().getFree() < fileSize ) { + if (availableQuota < fileSize ) { Log.w(TAG, "quota exceeded!"); return new RemoteOperationResult(ResultCode.QUOTA_EXCEEDED); } else { 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 72cee50c44d164f321e499f76523e008e4695769..0a52f2aa1a0649be8f354b814ffe3c192da37b50 100644 --- a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java +++ b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java @@ -30,7 +30,9 @@ import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.OnRemoteOperationListener; 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 java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; @@ -44,6 +46,7 @@ import foundation.e.drive.models.SyncRequest; import foundation.e.drive.models.SyncedFileState; import foundation.e.drive.operations.DownloadFileOperation; import foundation.e.drive.operations.RemoveFileOperation; +import foundation.e.drive.operations.UploadChunkedFileOperation; import foundation.e.drive.operations.UploadFileOperation; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; @@ -180,6 +183,8 @@ public class SynchronizationService extends Service implements OnRemoteOperation } } else if (callerOperation instanceof DownloadFileOperation) { Log.e(TAG, " Download: Unknown_error : failed"); + } else if(callerOperation instanceof UploadChunkedFileOperation) { + Log.e(TAG, "Chunked upload failed for unknown reason"); } break; case FORBIDDEN: @@ -215,7 +220,12 @@ public class SynchronizationService extends Service implements OnRemoteOperation switch (request.getOperationType()){ case UPLOAD: final SyncedFileState sfs = request.getSyncedFileState(); - operation = new UploadFileOperation(sfs, getApplicationContext()); + final File file = new File(sfs.getLocalPath()); + if (file.length() > 10240000) { //10 MB + operation = new UploadChunkedFileOperation(sfs, getApplicationContext()); + } else { + operation = new UploadFileOperation(sfs, getApplicationContext()); + } break; case DOWNLOAD: final DownloadRequest downloadRequest = (DownloadRequest) request;