|
@@ -0,0 +1,880 @@
|
|
|
+/*
|
|
|
+ * Nextcloud Android client application
|
|
|
+ *
|
|
|
+ * @author Tobias Kaminsky
|
|
|
+ * Copyright (C) 2017 Tobias Kaminsky
|
|
|
+ * Copyright (C) 2017 Nextcloud GmbH.
|
|
|
+ *
|
|
|
+ * This program is free software: you can redistribute it and/or modify
|
|
|
+ * it under the terms of the GNU Affero General Public License as published by
|
|
|
+ * the Free Software Foundation, either version 3 of the License, or
|
|
|
+ * at your option) any later version.
|
|
|
+ *
|
|
|
+ * This program is distributed in the hope that it will be useful,
|
|
|
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+ * GNU Affero General Public License for more details.
|
|
|
+ *
|
|
|
+ * You should have received a copy of the GNU Affero General Public License
|
|
|
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
+ */
|
|
|
+
|
|
|
+package com.owncloud.android.operations;
|
|
|
+
|
|
|
+import android.accounts.Account;
|
|
|
+import android.content.Context;
|
|
|
+import android.os.Build;
|
|
|
+import android.support.annotation.RequiresApi;
|
|
|
+
|
|
|
+import com.google.gson.reflect.TypeToken;
|
|
|
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
|
|
|
+import com.owncloud.android.datamodel.DecryptedFolderMetadata;
|
|
|
+import com.owncloud.android.datamodel.EncryptedFolderMetadata;
|
|
|
+import com.owncloud.android.datamodel.FileDataStorageManager;
|
|
|
+import com.owncloud.android.datamodel.OCFile;
|
|
|
+import com.owncloud.android.files.services.FileUploader;
|
|
|
+import com.owncloud.android.lib.common.OwnCloudClient;
|
|
|
+import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
|
|
|
+import com.owncloud.android.lib.common.network.ProgressiveDataTransferer;
|
|
|
+import com.owncloud.android.lib.common.operations.OperationCancelledException;
|
|
|
+import com.owncloud.android.lib.common.operations.RemoteOperation;
|
|
|
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|
|
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
|
|
|
+import com.owncloud.android.lib.common.utils.Log_OC;
|
|
|
+import com.owncloud.android.lib.resources.files.ChunkedUploadRemoteFileOperation;
|
|
|
+import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation;
|
|
|
+import com.owncloud.android.lib.resources.files.GetMetadataOperation;
|
|
|
+import com.owncloud.android.lib.resources.files.LockFileOperation;
|
|
|
+import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
|
|
|
+import com.owncloud.android.lib.resources.files.RemoteFile;
|
|
|
+import com.owncloud.android.lib.resources.files.StoreMetadataOperation;
|
|
|
+import com.owncloud.android.lib.resources.files.UnlockFileOperation;
|
|
|
+import com.owncloud.android.lib.resources.files.UpdateMetadataOperation;
|
|
|
+import com.owncloud.android.lib.resources.files.UploadRemoteFileOperation;
|
|
|
+import com.owncloud.android.operations.common.SyncOperation;
|
|
|
+import com.owncloud.android.utils.EncryptionUtils;
|
|
|
+import com.owncloud.android.utils.FileStorageUtils;
|
|
|
+import com.owncloud.android.utils.MimeType;
|
|
|
+
|
|
|
+import org.apache.commons.httpclient.HttpStatus;
|
|
|
+import org.apache.commons.httpclient.methods.RequestEntity;
|
|
|
+
|
|
|
+import java.io.File;
|
|
|
+import java.io.FileInputStream;
|
|
|
+import java.io.FileNotFoundException;
|
|
|
+import java.io.FileOutputStream;
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.channels.FileChannel;
|
|
|
+import java.nio.channels.FileLock;
|
|
|
+import java.nio.channels.OverlappingFileLockException;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.Iterator;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.UUID;
|
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
+
|
|
|
+import static com.owncloud.android.utils.EncryptionUtils.encodeStringToBase64Bytes;
|
|
|
+
|
|
|
+
|
|
|
+/**
|
|
|
+ * Operation performing the update in the ownCloud server
|
|
|
+ * of a file that was modified locally.
|
|
|
+ */
|
|
|
+@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
|
|
+public class UploadEncryptedFileOperation extends SyncOperation {
|
|
|
+
|
|
|
+ private static final String TAG = UploadEncryptedFileOperation.class.getSimpleName();
|
|
|
+
|
|
|
+ public static final int CREATED_BY_USER = 0;
|
|
|
+ public static final int CREATED_AS_INSTANT_PICTURE = 1;
|
|
|
+ public static final int CREATED_AS_INSTANT_VIDEO = 2;
|
|
|
+ private OCFile parentFile;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OCFile which is to be uploaded.
|
|
|
+ */
|
|
|
+ private OCFile ocFile;
|
|
|
+ private int localBehaviour = FileUploader.LOCAL_BEHAVIOUR_COPY;
|
|
|
+ private final String originalStoragePath;
|
|
|
+ private boolean chunked = false;
|
|
|
+ private boolean mRemoteFolderToBeCreated = false;
|
|
|
+ private int mCreatedBy = CREATED_BY_USER;
|
|
|
+
|
|
|
+ private long mOCUploadId = -1;
|
|
|
+ /**
|
|
|
+ * Local path to file which is to be uploaded (before any possible renaming or moving).
|
|
|
+ */
|
|
|
+ private Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<OnDatatransferProgressListener>();
|
|
|
+
|
|
|
+ private final AtomicBoolean mCancellationRequested = new AtomicBoolean(false);
|
|
|
+ private final AtomicBoolean mUploadStarted = new AtomicBoolean(false);
|
|
|
+
|
|
|
+ private Context context;
|
|
|
+
|
|
|
+ private UploadRemoteFileOperation mUploadOperation;
|
|
|
+
|
|
|
+ protected RequestEntity mEntity = null;
|
|
|
+
|
|
|
+ private Account account;
|
|
|
+ private ArbitraryDataProvider arbitraryDataProvider;
|
|
|
+
|
|
|
+ public UploadEncryptedFileOperation(OCFile parent, UploadFileOperation uploadFileOperation) {
|
|
|
+ parentFile = parent;
|
|
|
+ ocFile = uploadFileOperation.getFile();
|
|
|
+ account = uploadFileOperation.getAccount();
|
|
|
+ chunked = uploadFileOperation.isChunkedUploadSupported();
|
|
|
+ context = uploadFileOperation.getContext();
|
|
|
+ localBehaviour = uploadFileOperation.getLocalBehaviour();
|
|
|
+ originalStoragePath = uploadFileOperation.getStoragePath();
|
|
|
+
|
|
|
+ arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver());
|
|
|
+ }
|
|
|
+
|
|
|
+ public Account getAccount() {
|
|
|
+ return account;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getFileName() {
|
|
|
+ return (ocFile != null) ? ocFile.getFileName() : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ public OCFile getFile() {
|
|
|
+ return ocFile;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getStoragePath() {
|
|
|
+ return ocFile.getStoragePath();
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getRemotePath() {
|
|
|
+ return ocFile.getParentRemotePath() + ocFile.getEncryptedFileName();
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getMimeType() {
|
|
|
+ return ocFile.getMimetype();
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setRemoteFolderToBeCreated() {
|
|
|
+ mRemoteFolderToBeCreated = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setCreatedBy(int createdBy) {
|
|
|
+ mCreatedBy = createdBy;
|
|
|
+ if (createdBy < CREATED_BY_USER || CREATED_AS_INSTANT_VIDEO < createdBy) {
|
|
|
+ mCreatedBy = CREATED_BY_USER;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public int getCreatedBy() {
|
|
|
+ return mCreatedBy;
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean isInstantPicture() {
|
|
|
+ return mCreatedBy == CREATED_AS_INSTANT_PICTURE;
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean isInstantVideo() {
|
|
|
+ return mCreatedBy == CREATED_AS_INSTANT_VIDEO;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setOCUploadId(long id) {
|
|
|
+ mOCUploadId = id;
|
|
|
+ }
|
|
|
+
|
|
|
+ public long getOCUploadId() {
|
|
|
+ return mOCUploadId;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Set<OnDatatransferProgressListener> getDataTransferListeners() {
|
|
|
+ return mDataTransferListeners;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void addDatatransferProgressListener(OnDatatransferProgressListener listener) {
|
|
|
+ synchronized (mDataTransferListeners) {
|
|
|
+ mDataTransferListeners.add(listener);
|
|
|
+ }
|
|
|
+ if (mEntity != null) {
|
|
|
+ ((ProgressiveDataTransferer) mEntity).addDatatransferProgressListener(listener);
|
|
|
+ }
|
|
|
+ if (mUploadOperation != null) {
|
|
|
+ mUploadOperation.addDatatransferProgressListener(listener);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) {
|
|
|
+ synchronized (mDataTransferListeners) {
|
|
|
+ mDataTransferListeners.remove(listener);
|
|
|
+ }
|
|
|
+ if (mEntity != null) {
|
|
|
+ ((ProgressiveDataTransferer) mEntity).removeDatatransferProgressListener(listener);
|
|
|
+ }
|
|
|
+ if (mUploadOperation != null) {
|
|
|
+ mUploadOperation.removeDatatransferProgressListener(listener);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected RemoteOperationResult run(OwnCloudClient client) {
|
|
|
+ RemoteOperationResult result = null;
|
|
|
+ boolean metadataExists = false;
|
|
|
+ String token = null;
|
|
|
+ mCancellationRequested.set(false);
|
|
|
+ mUploadStarted.set(true);
|
|
|
+
|
|
|
+ File temporalFile = null;
|
|
|
+ File originalFile = new File(ocFile.getStoragePath());
|
|
|
+ File expectedFile = null;
|
|
|
+ FileLock fileLock = null;
|
|
|
+
|
|
|
+ String privateKey = arbitraryDataProvider.getValue(account.name, EncryptionUtils.PRIVATE_KEY);
|
|
|
+ String publicKey = arbitraryDataProvider.getValue(account.name, EncryptionUtils.PUBLIC_KEY);
|
|
|
+
|
|
|
+ try {
|
|
|
+
|
|
|
+ /// check if the file continues existing before schedule the operation
|
|
|
+ if (!originalFile.exists()) {
|
|
|
+ Log_OC.d(TAG, ocFile.getStoragePath() + " not exists anymore");
|
|
|
+ throw new FileNotFoundException();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// check the existence of the parent folder for the file to upload
|
|
|
+ String remoteParentPath = new File(getRemotePath()).getParent();
|
|
|
+ remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ?
|
|
|
+ remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR;
|
|
|
+ result = grantFolderExistence(remoteParentPath, client);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // TODO automatic rename? UploadFileOperation:365
|
|
|
+
|
|
|
+ /// set parent local id in uploading file
|
|
|
+// OCFile parent = getStorageManager().getFileByPath(remoteParentPath);
|
|
|
+// ocFile.setParentId(parent.getFileId());
|
|
|
+
|
|
|
+
|
|
|
+// if (mCancellationRequested.get()) {
|
|
|
+// throw new OperationCancelledException();
|
|
|
+// }
|
|
|
+
|
|
|
+ // Get the last modification date of the file from the file system
|
|
|
+ Long timeStampLong = originalFile.lastModified() / 1000;
|
|
|
+ String timeStamp = timeStampLong.toString();
|
|
|
+
|
|
|
+ // Lock folder
|
|
|
+ LockFileOperation lockFileOperation = new LockFileOperation(parentFile.getLocalId());
|
|
|
+ RemoteOperationResult lockFileOperationResult = lockFileOperation.execute(client);
|
|
|
+
|
|
|
+
|
|
|
+ if (lockFileOperationResult.isSuccess()) {
|
|
|
+ token = (String) lockFileOperationResult.getData().get(0);
|
|
|
+ } else if (lockFileOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
|
|
|
+ throw new Exception("Forbidden! Please try again later.)");
|
|
|
+ } else {
|
|
|
+ throw new Exception("Unknown error!");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update metadata
|
|
|
+ GetMetadataOperation getMetadataOperation = new GetMetadataOperation(parentFile.getLocalId());
|
|
|
+ RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client);
|
|
|
+
|
|
|
+ DecryptedFolderMetadata metadata;
|
|
|
+
|
|
|
+ if (getMetadataOperationResult.isSuccess()) {
|
|
|
+ metadataExists = true;
|
|
|
+
|
|
|
+ // decrypt metadata
|
|
|
+ String serializedEncryptedMetadata = (String) getMetadataOperationResult.getData().get(0);
|
|
|
+
|
|
|
+
|
|
|
+ EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
|
|
|
+ serializedEncryptedMetadata, new TypeToken<EncryptedFolderMetadata>() {
|
|
|
+ });
|
|
|
+
|
|
|
+ metadata = EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, privateKey);
|
|
|
+
|
|
|
+ } else if (getMetadataOperationResult.getHttpCode() == HttpStatus.SC_NOT_FOUND) {
|
|
|
+ // new metadata
|
|
|
+ metadata = new DecryptedFolderMetadata();
|
|
|
+ metadata.metadata = new DecryptedFolderMetadata.Metadata();
|
|
|
+ metadata.metadata.metadataKeys = new HashMap<>();
|
|
|
+ String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey());
|
|
|
+ String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey);
|
|
|
+ metadata.metadata.metadataKeys.put(0, encryptedMetadataKey);
|
|
|
+ } else {
|
|
|
+ // TODO error
|
|
|
+ throw new Exception("something wrong");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Key
|
|
|
+ byte[] key = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // TODO change key if file has changed, e.g. when file is updated
|
|
|
+ key = encodeStringToBase64Bytes(metadata.files.get(ocFile.getFileName()).encrypted.key);
|
|
|
+ } catch (Exception e) {
|
|
|
+ // no key found
|
|
|
+ }
|
|
|
+
|
|
|
+ if (key == null || key.length == 0) {
|
|
|
+ key = EncryptionUtils.generateKey();
|
|
|
+ }
|
|
|
+
|
|
|
+ // IV
|
|
|
+ byte[] iv = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ iv = encodeStringToBase64Bytes(metadata.files.get(ocFile.getFileName()).initializationVector);
|
|
|
+ } catch (Exception e) {
|
|
|
+ // no iv found
|
|
|
+ }
|
|
|
+
|
|
|
+ if (iv == null || iv.length == 0) {
|
|
|
+ iv = EncryptionUtils.generateIV();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ EncryptionUtils.EncryptedFile encryptedFile = EncryptionUtils.encryptFile(ocFile, key, iv);
|
|
|
+
|
|
|
+ // new random file name, check if it exists in metadata
|
|
|
+ String encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
|
|
|
+
|
|
|
+ while (metadata.files.get(encryptedFileName) != null) {
|
|
|
+ encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
|
|
|
+ }
|
|
|
+
|
|
|
+ ocFile.setEncryptedFileName(encryptedFileName);
|
|
|
+
|
|
|
+ File encryptedTempFile = File.createTempFile("encFile", encryptedFileName);
|
|
|
+ FileOutputStream fileOutputStream = new FileOutputStream(encryptedTempFile);
|
|
|
+ fileOutputStream.write(encryptedFile.encryptedBytes);
|
|
|
+ fileOutputStream.close();
|
|
|
+
|
|
|
+ /// perform the upload
|
|
|
+ if (chunked &&
|
|
|
+ (new File(ocFile.getStoragePath())).length() >
|
|
|
+ ChunkedUploadRemoteFileOperation.CHUNK_SIZE) {
|
|
|
+ mUploadOperation = new ChunkedUploadRemoteFileOperation(context, encryptedTempFile.getAbsolutePath(),
|
|
|
+ ocFile.getParentRemotePath() + encryptedFileName, ocFile.getMimetype(),
|
|
|
+ ocFile.getEtagInConflict(), timeStamp);
|
|
|
+ } else {
|
|
|
+ mUploadOperation = new UploadRemoteFileOperation(encryptedTempFile.getAbsolutePath(),
|
|
|
+ ocFile.getParentRemotePath() + encryptedFileName, ocFile.getMimetype(),
|
|
|
+ ocFile.getEtagInConflict(), timeStamp);
|
|
|
+ }
|
|
|
+
|
|
|
+ Iterator<OnDatatransferProgressListener> listener = mDataTransferListeners.iterator();
|
|
|
+ while (listener.hasNext()) {
|
|
|
+ mUploadOperation.addDatatransferProgressListener(listener.next());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mCancellationRequested.get()) {
|
|
|
+ throw new OperationCancelledException();
|
|
|
+ }
|
|
|
+
|
|
|
+// FileChannel channel = null;
|
|
|
+// try {
|
|
|
+// channel = new RandomAccessFile(ocFile.getStoragePath(), "rw").getChannel();
|
|
|
+// fileLock = channel.tryLock();
|
|
|
+// } catch (FileNotFoundException e) {
|
|
|
+// if (temporalFile == null) {
|
|
|
+// String temporalPath = FileStorageUtils.getTemporalPath(account.name) + ocFile.getRemotePath();
|
|
|
+// ocFile.setStoragePath(temporalPath);
|
|
|
+// temporalFile = new File(temporalPath);
|
|
|
+//
|
|
|
+// result = copy(originalFile, temporalFile);
|
|
|
+//
|
|
|
+// if (result != null) {
|
|
|
+// return result;
|
|
|
+// } else {
|
|
|
+// if (temporalFile.length() == originalFile.length()) {
|
|
|
+// channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel();
|
|
|
+// fileLock = channel.tryLock();
|
|
|
+// } else {
|
|
|
+// while (temporalFile.length() != originalFile.length()) {
|
|
|
+// Files.deleteIfExists(Paths.get(temporalPath));
|
|
|
+// result = copy(originalFile, temporalFile);
|
|
|
+//
|
|
|
+// if (result != null) {
|
|
|
+// return result;
|
|
|
+// } else {
|
|
|
+// channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").
|
|
|
+// getChannel();
|
|
|
+// fileLock = channel.tryLock();
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// } else {
|
|
|
+// channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel();
|
|
|
+// fileLock = channel.tryLock();
|
|
|
+// }
|
|
|
+// }
|
|
|
+
|
|
|
+ result = mUploadOperation.execute(client);
|
|
|
+
|
|
|
+ /// move local temporal file or original file to its corresponding
|
|
|
+ // location in the ownCloud local folder
|
|
|
+ if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED) {
|
|
|
+ result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (result.isSuccess()) {
|
|
|
+ // upload metadata
|
|
|
+ DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
|
|
|
+ DecryptedFolderMetadata.Data data = new DecryptedFolderMetadata.Data();
|
|
|
+ data.filename = ocFile.getFileName();
|
|
|
+ data.mimetype = ocFile.getMimetype();
|
|
|
+ data.key = EncryptionUtils.encodeBytesToBase64String(key);
|
|
|
+
|
|
|
+ decryptedFile.encrypted = data;
|
|
|
+ decryptedFile.initializationVector = EncryptionUtils.encodeBytesToBase64String(iv);
|
|
|
+ decryptedFile.authenticationTag = encryptedFile.authenticationTag;
|
|
|
+
|
|
|
+ metadata.files.put(encryptedFileName, decryptedFile);
|
|
|
+
|
|
|
+ EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
|
|
|
+ privateKey);
|
|
|
+ String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
|
|
|
+
|
|
|
+ // upload metadata
|
|
|
+ RemoteOperationResult uploadMetadataOperationResult;
|
|
|
+ if (metadataExists) {
|
|
|
+ // update metadata
|
|
|
+ UpdateMetadataOperation storeMetadataOperation = new UpdateMetadataOperation(parentFile.getLocalId(),
|
|
|
+ serializedFolderMetadata, token);
|
|
|
+ uploadMetadataOperationResult = storeMetadataOperation.execute(client);
|
|
|
+ } else {
|
|
|
+ // store metadata
|
|
|
+ StoreMetadataOperation storeMetadataOperation = new StoreMetadataOperation(parentFile.getLocalId(),
|
|
|
+ serializedFolderMetadata);
|
|
|
+ uploadMetadataOperationResult = storeMetadataOperation.execute(client);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!uploadMetadataOperationResult.isSuccess()) {
|
|
|
+ throw new Exception();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (FileNotFoundException e) {
|
|
|
+ Log_OC.d(TAG, ocFile.getStoragePath() + " not exists anymore");
|
|
|
+ result = new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND);
|
|
|
+ } catch (OverlappingFileLockException e) {
|
|
|
+ Log_OC.d(TAG, "Overlapping file lock exception");
|
|
|
+ result = new RemoteOperationResult(ResultCode.LOCK_FAILED);
|
|
|
+ } catch (Exception e) {
|
|
|
+ result = new RemoteOperationResult(e);
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ mUploadStarted.set(false);
|
|
|
+
|
|
|
+ // unlock file
|
|
|
+ if (token != null) {
|
|
|
+ UnlockFileOperation unlockFileOperation = new UnlockFileOperation(parentFile.getLocalId(), token);
|
|
|
+ RemoteOperationResult unlockFileOperationResult = unlockFileOperation.execute(client);
|
|
|
+
|
|
|
+ if (!unlockFileOperationResult.isSuccess()) {
|
|
|
+ Log_OC.e(TAG, "Failed to unlock " + parentFile.getLocalId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fileLock != null) {
|
|
|
+ try {
|
|
|
+ fileLock.release();
|
|
|
+ } catch (IOException e) {
|
|
|
+ Log_OC.e(TAG, "Failed to unlock file with path " + ocFile.getStoragePath());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (temporalFile != null && !originalFile.equals(temporalFile)) {
|
|
|
+ temporalFile.delete();
|
|
|
+ }
|
|
|
+ if (result == null) {
|
|
|
+ result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR);
|
|
|
+ }
|
|
|
+ if (result.isSuccess()) {
|
|
|
+ Log_OC.i(TAG, "Upload of " + ocFile.getStoragePath() + " to " + ocFile.getRemotePath() + ": " +
|
|
|
+ result.getLogMessage());
|
|
|
+ } else {
|
|
|
+ if (result.getException() != null) {
|
|
|
+ if (result.isCancelled()) {
|
|
|
+ Log_OC.w(TAG, "Upload of " + ocFile.getStoragePath() + " to " + ocFile.getRemotePath() +
|
|
|
+ ": " + result.getLogMessage());
|
|
|
+ } else {
|
|
|
+ Log_OC.e(TAG, "Upload of " + ocFile.getStoragePath() + " to " + ocFile.getRemotePath() +
|
|
|
+ ": " + result.getLogMessage(), result.getException());
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+ Log_OC.e(TAG, "Upload of " + ocFile.getStoragePath() + " to " + ocFile.getRemotePath() +
|
|
|
+ ": " + result.getLogMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (localBehaviour) {
|
|
|
+ case FileUploader.LOCAL_BEHAVIOUR_FORGET:
|
|
|
+ String temporalPath = FileStorageUtils.getTemporalPath(account.name) + ocFile.getRemotePath();
|
|
|
+ if (originalStoragePath.equals(temporalPath)) {
|
|
|
+ // delete local file is was pre-copied in temporary folder (see .ui.helpers.UriUploader)
|
|
|
+ temporalFile = new File(temporalPath);
|
|
|
+ temporalFile.delete();
|
|
|
+ }
|
|
|
+ ocFile.setStoragePath("");
|
|
|
+ saveUploadedFile(client);
|
|
|
+ break;
|
|
|
+
|
|
|
+ case FileUploader.LOCAL_BEHAVIOUR_DELETE:
|
|
|
+ Log_OC.d(TAG, "Delete source file");
|
|
|
+
|
|
|
+ originalFile.delete();
|
|
|
+ getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
|
|
|
+ saveUploadedFile(client);
|
|
|
+ break;
|
|
|
+
|
|
|
+ case FileUploader.LOCAL_BEHAVIOUR_COPY:
|
|
|
+ if (temporalFile != null) {
|
|
|
+ try {
|
|
|
+ move(temporalFile, expectedFile);
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ocFile.setStoragePath(expectedFile.getAbsolutePath());
|
|
|
+ saveUploadedFile(client);
|
|
|
+ FileDataStorageManager.triggerMediaScan(expectedFile.getAbsolutePath());
|
|
|
+ break;
|
|
|
+
|
|
|
+ case FileUploader.LOCAL_BEHAVIOUR_MOVE:
|
|
|
+
|
|
|
+ String expectedPath = FileStorageUtils.getDefaultSavePathFor(account.name, ocFile);
|
|
|
+ expectedFile = new File(expectedPath);
|
|
|
+
|
|
|
+ try {
|
|
|
+ move(originalFile, expectedFile);
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
|
|
|
+ ocFile.setStoragePath(expectedFile.getAbsolutePath());
|
|
|
+ saveUploadedFile(client);
|
|
|
+ FileDataStorageManager.triggerMediaScan(expectedFile.getAbsolutePath());
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Checks the existence of the folder where the current file will be uploaded both
|
|
|
+ * in the remote server and in the local database.
|
|
|
+ * <p/>
|
|
|
+ * If the upload is set to enforce the creation of the folder, the method tries to
|
|
|
+ * create it both remote and locally.
|
|
|
+ *
|
|
|
+ * @param pathToGrant Full remote path whose existence will be granted.
|
|
|
+ * @return An {@link OCFile} instance corresponding to the folder where the file
|
|
|
+ * will be uploaded.
|
|
|
+ */
|
|
|
+ private RemoteOperationResult grantFolderExistence(String pathToGrant, OwnCloudClient client) {
|
|
|
+ RemoteOperation operation = new ExistenceCheckRemoteOperation(pathToGrant, context, false);
|
|
|
+ RemoteOperationResult result = operation.execute(client);
|
|
|
+ if (!result.isSuccess() && result.getCode() == ResultCode.FILE_NOT_FOUND && mRemoteFolderToBeCreated) {
|
|
|
+ SyncOperation syncOp = new CreateFolderOperation(pathToGrant, true);
|
|
|
+ result = syncOp.execute(client, getStorageManager());
|
|
|
+ }
|
|
|
+ if (result.isSuccess()) {
|
|
|
+ OCFile parentDir = getStorageManager().getFileByPath(pathToGrant);
|
|
|
+ if (parentDir == null) {
|
|
|
+ parentDir = createLocalFolder(pathToGrant);
|
|
|
+ }
|
|
|
+ if (parentDir != null) {
|
|
|
+ result = new RemoteOperationResult(ResultCode.OK);
|
|
|
+ } else {
|
|
|
+ result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private OCFile createLocalFolder(String remotePath) {
|
|
|
+ String parentPath = new File(remotePath).getParent();
|
|
|
+ parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ?
|
|
|
+ parentPath : parentPath + OCFile.PATH_SEPARATOR;
|
|
|
+ OCFile parent = getStorageManager().getFileByPath(parentPath);
|
|
|
+ if (parent == null) {
|
|
|
+ parent = createLocalFolder(parentPath);
|
|
|
+ }
|
|
|
+ if (parent != null) {
|
|
|
+ OCFile createdFolder = new OCFile(remotePath);
|
|
|
+ createdFolder.setMimetype(MimeType.DIRECTORY);
|
|
|
+ createdFolder.setParentId(parent.getFileId());
|
|
|
+ getStorageManager().saveFile(createdFolder);
|
|
|
+ return createdFolder;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Checks if remotePath does not exist in the server and returns it, or adds
|
|
|
+ * a suffix to it in order to avoid the server file is overwritten.
|
|
|
+ *
|
|
|
+ * @param wc
|
|
|
+ * @param remotePath
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private String getAvailableRemotePath(OwnCloudClient wc, String remotePath) {
|
|
|
+ boolean check = existsFile(wc, remotePath);
|
|
|
+ if (!check) {
|
|
|
+ return remotePath;
|
|
|
+ }
|
|
|
+
|
|
|
+ int pos = remotePath.lastIndexOf('.');
|
|
|
+ String suffix = "";
|
|
|
+ String extension = "";
|
|
|
+ if (pos >= 0) {
|
|
|
+ extension = remotePath.substring(pos + 1);
|
|
|
+ remotePath = remotePath.substring(0, pos);
|
|
|
+ }
|
|
|
+ int count = 2;
|
|
|
+ do {
|
|
|
+ suffix = " (" + count + ")";
|
|
|
+ if (pos >= 0) {
|
|
|
+ check = existsFile(wc, remotePath + suffix + "." + extension);
|
|
|
+ } else {
|
|
|
+ check = existsFile(wc, remotePath + suffix);
|
|
|
+ }
|
|
|
+ count++;
|
|
|
+ } while (check);
|
|
|
+
|
|
|
+ if (pos >= 0) {
|
|
|
+ return remotePath + suffix + "." + extension;
|
|
|
+ } else {
|
|
|
+ return remotePath + suffix;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean existsFile(OwnCloudClient client, String remotePath) {
|
|
|
+ ExistenceCheckRemoteOperation existsOperation =
|
|
|
+ new ExistenceCheckRemoteOperation(remotePath, context, false);
|
|
|
+ RemoteOperationResult result = existsOperation.execute(client);
|
|
|
+ return result.isSuccess();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Allows to cancel the actual upload operation. If actual upload operating
|
|
|
+ * is in progress it is cancelled, if upload preparation is being performed
|
|
|
+ * upload will not take place.
|
|
|
+ */
|
|
|
+ public void cancel() {
|
|
|
+ if (mUploadOperation == null) {
|
|
|
+ if (mUploadStarted.get()) {
|
|
|
+ Log_OC.d(TAG, "Cancelling upload during upload preparations.");
|
|
|
+ mCancellationRequested.set(true);
|
|
|
+ } else {
|
|
|
+ Log_OC.e(TAG, "No upload in progress. This should not happen.");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ Log_OC.d(TAG, "Cancelling upload during actual upload operation.");
|
|
|
+ mUploadOperation.cancel();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * As soon as this method return true, upload can be cancel via cancel().
|
|
|
+ */
|
|
|
+ public boolean isUploadInProgress() {
|
|
|
+ return mUploadStarted.get();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+// /**
|
|
|
+// * TODO rewrite with homogeneous fail handling, remove dependency on {@link RemoteOperationResult},
|
|
|
+// * TODO use Exceptions instead
|
|
|
+// *
|
|
|
+// * @param sourceFile Source file to copy.
|
|
|
+// * @param targetFile Target location to copy the file.
|
|
|
+// * @return {@link RemoteOperationResult}
|
|
|
+// * @throws IOException
|
|
|
+// */
|
|
|
+// private RemoteOperationResult copy(File sourceFile, File targetFile) throws IOException {
|
|
|
+// Log_OC.d(TAG, "Copying local file");
|
|
|
+//
|
|
|
+// RemoteOperationResult result = null;
|
|
|
+//
|
|
|
+// if (FileStorageUtils.getUsableSpace(account.name) < sourceFile.length()) {
|
|
|
+// result = new RemoteOperationResult(ResultCode.LOCAL_STORAGE_FULL);
|
|
|
+// return result; // error condition when the file should be copied
|
|
|
+//
|
|
|
+// } else {
|
|
|
+// Log_OC.d(TAG, "Creating temporal folder");
|
|
|
+// File temporalParent = targetFile.getParentFile();
|
|
|
+// temporalParent.mkdirs();
|
|
|
+// if (!temporalParent.isDirectory()) {
|
|
|
+// throw new IOException(
|
|
|
+// "Unexpected error: parent directory could not be created");
|
|
|
+// }
|
|
|
+// Log_OC.d(TAG, "Creating temporal file");
|
|
|
+// targetFile.createNewFile();
|
|
|
+// if (!targetFile.isFile()) {
|
|
|
+// throw new IOException(
|
|
|
+// "Unexpected error: target file could not be created");
|
|
|
+// }
|
|
|
+//
|
|
|
+// Log_OC.d(TAG, "Copying file contents");
|
|
|
+// InputStream in = null;
|
|
|
+// OutputStream out = null;
|
|
|
+//
|
|
|
+// try {
|
|
|
+// if (!mOriginalStoragePath.equals(targetFile.getAbsolutePath())) {
|
|
|
+// // In case document provider schema as 'content://'
|
|
|
+// if (mOriginalStoragePath.startsWith(UriUtils.URI_CONTENT_SCHEME)) {
|
|
|
+// Uri uri = Uri.parse(mOriginalStoragePath);
|
|
|
+// in = context.getContentResolver().openInputStream(uri);
|
|
|
+// } else {
|
|
|
+// in = new FileInputStream(sourceFile);
|
|
|
+// }
|
|
|
+// out = new FileOutputStream(targetFile);
|
|
|
+// int nRead;
|
|
|
+// byte[] buf = new byte[4096];
|
|
|
+// while (!mCancellationRequested.get() &&
|
|
|
+// (nRead = in.read(buf)) > -1) {
|
|
|
+// out.write(buf, 0, nRead);
|
|
|
+// }
|
|
|
+// out.flush();
|
|
|
+//
|
|
|
+// } // else: weird but possible situation, nothing to copy
|
|
|
+//
|
|
|
+// if (mCancellationRequested.get()) {
|
|
|
+// result = new RemoteOperationResult(new OperationCancelledException());
|
|
|
+// return result;
|
|
|
+// }
|
|
|
+//
|
|
|
+// } catch (Exception e) {
|
|
|
+// result = new RemoteOperationResult(ResultCode.LOCAL_STORAGE_NOT_COPIED);
|
|
|
+// return result;
|
|
|
+//
|
|
|
+// } finally {
|
|
|
+// try {
|
|
|
+// if (in != null) {
|
|
|
+// in.close();
|
|
|
+// }
|
|
|
+// } catch (Exception e) {
|
|
|
+// Log_OC.d(TAG, "Weird exception while closing input stream for " +
|
|
|
+// mOriginalStoragePath + " (ignoring)", e);
|
|
|
+// }
|
|
|
+// try {
|
|
|
+// if (out != null) {
|
|
|
+// out.close();
|
|
|
+// }
|
|
|
+// } catch (Exception e) {
|
|
|
+// Log_OC.d(TAG, "Weird exception while closing output stream for " +
|
|
|
+// targetFile.getAbsolutePath() + " (ignoring)", e);
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// return result;
|
|
|
+// }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * TODO rewrite with homogeneous fail handling, remove dependency on {@link RemoteOperationResult},
|
|
|
+ * TODO use Exceptions instead
|
|
|
+ * <p>
|
|
|
+ * TODO refactor both this and 'copy' in a single method
|
|
|
+ *
|
|
|
+ * @param sourceFile Source file to move.
|
|
|
+ * @param targetFile Target location to move the file.
|
|
|
+ * @return {@link RemoteOperationResult}
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ private void move(File sourceFile, File targetFile) throws IOException {
|
|
|
+
|
|
|
+ if (!targetFile.equals(sourceFile)) {
|
|
|
+ File expectedFolder = targetFile.getParentFile();
|
|
|
+ expectedFolder.mkdirs();
|
|
|
+
|
|
|
+ if (expectedFolder.isDirectory()) {
|
|
|
+ if (!sourceFile.renameTo(targetFile)) {
|
|
|
+ // try to copy and then delete
|
|
|
+ targetFile.createNewFile();
|
|
|
+ FileChannel inChannel = new FileInputStream(sourceFile).getChannel();
|
|
|
+ FileChannel outChannel = new FileOutputStream(targetFile).getChannel();
|
|
|
+ try {
|
|
|
+ inChannel.transferTo(0, inChannel.size(), outChannel);
|
|
|
+ sourceFile.delete();
|
|
|
+ } catch (Exception e) {
|
|
|
+ ocFile.setStoragePath(""); // forget the local file
|
|
|
+ // by now, treat this as a success; the file was uploaded
|
|
|
+ // the best option could be show a warning message
|
|
|
+ } finally {
|
|
|
+ if (inChannel != null) {
|
|
|
+ inChannel.close();
|
|
|
+ }
|
|
|
+ if (outChannel != null) {
|
|
|
+ outChannel.close();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+ ocFile.setStoragePath("");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Saves a OC File after a successful upload.
|
|
|
+ * <p/>
|
|
|
+ * A PROPFIND is necessary to keep the props in the local database
|
|
|
+ * synchronized with the server, specially the modification time and Etag
|
|
|
+ * (where available)
|
|
|
+ * <p/>
|
|
|
+ */
|
|
|
+ private void saveUploadedFile(OwnCloudClient client) {
|
|
|
+ OCFile file = ocFile;
|
|
|
+ if (file.fileExists()) {
|
|
|
+ file = getStorageManager().getFileById(file.getFileId());
|
|
|
+ }
|
|
|
+ long syncDate = System.currentTimeMillis();
|
|
|
+ file.setLastSyncDateForData(syncDate);
|
|
|
+
|
|
|
+ // new PROPFIND to keep data consistent with server
|
|
|
+ // in theory, should return the same we already have
|
|
|
+ // TODO from the appropriate OC server version, get data from last PUT response headers, instead
|
|
|
+ // TODO of a new PROPFIND; the latter may fail, specially for chunked uploads
|
|
|
+ ReadRemoteFileOperation operation = new ReadRemoteFileOperation(getRemotePath());
|
|
|
+ RemoteOperationResult result = operation.execute(client);
|
|
|
+ if (result.isSuccess()) {
|
|
|
+ updateOCFile(file, (RemoteFile) result.getData().get(0));
|
|
|
+ file.setLastSyncDateForProperties(syncDate);
|
|
|
+ } else {
|
|
|
+ Log_OC.e(TAG, "Error reading properties of file after successful upload; this is gonna hurt...");
|
|
|
+ }
|
|
|
+
|
|
|
+ file.setEncrypted(true);
|
|
|
+ file.setStoragePath("");
|
|
|
+ file.setParentId(parentFile.getFileId());
|
|
|
+ getStorageManager().saveFile(file);
|
|
|
+ getStorageManager().saveConflict(file, null);
|
|
|
+
|
|
|
+ FileDataStorageManager.triggerMediaScan(file.getStoragePath());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void updateOCFile(OCFile file, RemoteFile remoteFile) {
|
|
|
+ file.setCreationTimestamp(remoteFile.getCreationTimestamp());
|
|
|
+ file.setFileLength(remoteFile.getLength());
|
|
|
+// file.setMimetype(file.getMimetype());
|
|
|
+ file.setModificationTimestamp(remoteFile.getModifiedTimestamp());
|
|
|
+ file.setModificationTimestampAtLastSyncForData(remoteFile.getModifiedTimestamp());
|
|
|
+ file.setEtag(remoteFile.getEtag());
|
|
|
+ file.setRemoteId(remoteFile.getRemoteId());
|
|
|
+ }
|
|
|
+
|
|
|
+ public interface OnRenameListener {
|
|
|
+
|
|
|
+ void onRenameUpload();
|
|
|
+ }
|
|
|
+
|
|
|
+}
|