Prechádzať zdrojové kódy

First release of E2E

Jan-Christoph Borchardt 7 rokov pred
rodič
commit
45bcdebd9d

+ 2 - 2
src/main/AndroidManifest.xml

@@ -20,8 +20,8 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.owncloud.android"
-          android:versionCode="20000009"
-          android:versionName="2.0.0RC9">
+          android:versionCode="20000052"
+          android:versionName="2.0.0-e2e-02">
 
     <!-- GET_ACCOUNTS is needed for API <= 22.
         For API >= 23 results in the addition of CONTACTS group to the list of permissions that may be

+ 3 - 0
src/main/java/com/owncloud/android/MainApp.java

@@ -64,6 +64,7 @@ import com.owncloud.android.utils.AnalyticsUtils;
 import com.owncloud.android.utils.FilesSyncHelper;
 import com.owncloud.android.utils.PermissionUtil;
 import com.owncloud.android.utils.ReceiversHelper;
+import com.owncloud.android.utils.EncryptionUtils;
 
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -71,6 +72,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import javax.crypto.Cipher;
+
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 

+ 13 - 2
src/main/java/com/owncloud/android/files/services/FileUploader.java

@@ -1055,8 +1055,19 @@ public class FileUploader extends Service
                 mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().
                         getClientFor(ocAccount, this);
 
-                /// perform the upload
-                uploadResult = mCurrentUpload.execute(mUploadClient, mStorageManager);
+
+//                // If parent folder is encrypted, upload file encrypted
+//                OCFile parent = mStorageManager.getFileByPath(mCurrentUpload.getFile().getParentRemotePath());
+
+//                if (parent.isEncrypted()) {
+//                    UploadEncryptedFileOperation uploadEncryptedFileOperation =
+//                            new UploadEncryptedFileOperation(parent, mCurrentUpload);
+//
+//                    uploadResult = uploadEncryptedFileOperation.execute(mUploadClient, mStorageManager);
+//                } else {
+                    /// perform the regular upload
+                    uploadResult = mCurrentUpload.execute(mUploadClient, mStorageManager);
+//                }
 
 
             } catch (Exception e) {

+ 880 - 0
src/main/java/com/owncloud/android/operations/UploadEncryptedFileOperation.java

@@ -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();
+    }
+
+}

+ 73 - 13
src/main/java/com/owncloud/android/operations/UploadFileOperation.java

@@ -87,6 +87,8 @@ 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
@@ -408,7 +410,7 @@ public class UploadFileOperation extends SyncOperation {
         File originalFile = new File(mOriginalStoragePath);
         File expectedFile = null;
         FileLock fileLock = null;
-        long size;
+        long size = 0;
 
         boolean metadataExists = false;
         String token = null;
@@ -418,8 +420,50 @@ public class UploadFileOperation extends SyncOperation {
         String privateKey = arbitraryDataProvider.getValue(getAccount().name, EncryptionUtils.PRIVATE_KEY);
         String publicKey = arbitraryDataProvider.getValue(getAccount().name, EncryptionUtils.PUBLIC_KEY);
 
-        try {
+        /// 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;
+        RemoteOperationResult result = grantFolderExistence(remoteParentPath, client);
+
+        if (!result.isSuccess()) {
+            return result;
+        }
+
+        OCFile parent = getStorageManager().getFileByPath(remoteParentPath);
+        mFile.setParentId(parent.getFileId());
+
+        if (parent.isEncrypted()) {
+            Log_OC.d(TAG, "encrypted upload");
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+                return encryptedUpload(client, parent);
+            } else {
+                Log_OC.e(TAG, "Encrypted upload on old Android API");
+                return new RemoteOperationResult(ResultCode.OLD_ANDROID_API);
+            }
+        } else {
+            Log_OC.d(TAG, "normal upload");
+            return normalUpload(client);
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile parentFile) {
+        RemoteOperationResult result = null;
+        File temporalFile = null;
+        File originalFile = new File(mOriginalStoragePath);
+        File expectedFile = null;
+        FileLock fileLock = null;
 
+        boolean metadataExists = false;
+        String token = null;
+
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContext().getContentResolver());
+
+        String privateKey = arbitraryDataProvider.getValue(getAccount().name, EncryptionUtils.PRIVATE_KEY);
+        String publicKey = arbitraryDataProvider.getValue(getAccount().name, EncryptionUtils.PUBLIC_KEY);
+
+        try {
             // check conditions 
             result = checkConditions(originalFile);
 
@@ -488,11 +532,33 @@ public class UploadFileOperation extends SyncOperation {
 
             /***** E2E *****/
 
-            // Key, always generate new one
-            byte[] key = EncryptionUtils.generateKey();
+            // Key
+            byte[] key = null;
+
+            try {
+                // TODO change key if file has changed, e.g. when file is updated
+                key = encodeStringToBase64Bytes(metadata.files.get(mFile.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(mFile.getFileName()).initializationVector);
+            } catch (Exception e) {
+                // no iv found
+            }
+
+            if (iv == null || iv.length == 0) {
+                iv = EncryptionUtils.generateIV();
+            }
 
-            // IV, always generate new one
-            byte[] iv = EncryptionUtils.generateIV();
 
             EncryptionUtils.EncryptedFile encryptedFile = EncryptionUtils.encryptFile(mFile, key, iv);
 
@@ -572,8 +638,7 @@ public class UploadFileOperation extends SyncOperation {
 
 //            FileChannel channel = null;
 //try {
-
-    //                channel = new RandomAccessFile(ocFile.getStoragePath(), "rw").getChannel();
+//                channel = new RandomAccessFile(ocFile.getStoragePath(), "rw").getChannel();
 //                fileLock = channel.tryLock();
 //            } catch (FileNotFoundException e) {
 //                if (temporalFile == null) {
@@ -610,11 +675,6 @@ public class UploadFileOperation extends SyncOperation {
 //                }
 //            }
 
-//            boolean test = true;
-//            if (test) {
-//                throw new Exception("test");
-//            }
-            
             result = mUploadOperation.execute(client);
 //            if (result == null || result.isSuccess() && mUploadOperation != null) {
 //                result = mUploadOperation.execute(client);

+ 1 - 0
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -52,6 +52,7 @@ import android.support.v4.view.MenuItemCompat;
 import android.support.v7.app.AlertDialog;
 import android.support.v7.widget.SearchView;
 import android.text.TextUtils;
+import android.util.Log;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;

+ 2 - 0
src/main/res/values/strings.xml

@@ -270,6 +270,8 @@
     <string name="favorite_switch">Available offline</string>
     <string name="encrypted">Set as encrypted</string>
     <string name="unset_encrypted">Unset encryption</string>
+    <string name="encrypted">Set as encrypted</string>
+    <string name="unset_encrypted">Unset encryption</string>
     <string name="common_rename">Rename</string>
     <string name="common_remove">Delete</string>
     <string name="confirmation_remove_file_alert">Do you really want to delete %1$s?</string>

+ 2 - 2
src/modified/AndroidManifest.xml

@@ -19,8 +19,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools"
           package="com.owncloud.android"
-          android:versionCode="20000009"
-          android:versionName="2.0.0RC9">
+          android:versionCode="20000052"
+          android:versionName="2.0.0-e2e-02">
 
     <application
         android:name=".MainApp"

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 57 - 0
src/test/java/com/owncloud/android/utils/EncryptionTest.java


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov