/** * ownCloud Android client application * * @author David A. Velasco * Copyright (C) 2016 ownCloud Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package com.owncloud.android.operations; import android.accounts.Account; import android.content.Context; import android.net.Uri; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.db.OCUpload; import com.owncloud.android.db.PreferenceManager; 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.ReadRemoteFileOperation; import com.owncloud.android.lib.resources.files.RemoteFile; import com.owncloud.android.lib.resources.files.UploadRemoteFileOperation; import com.owncloud.android.operations.common.SyncOperation; import com.owncloud.android.utils.ConnectivityUtils; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.MimeType; import com.owncloud.android.utils.MimetypeIconUtil; import com.owncloud.android.utils.UriUtils; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.RequestEntity; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.channels.FileChannel; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** * Operation performing the update in the ownCloud server * of a file that was modified locally. */ public class UploadFileOperation extends SyncOperation { 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; public static OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType) { // MIME type if (mimeType == null || mimeType.length() <= 0) { mimeType = MimetypeIconUtil.getBestMimeTypeByFilename(localPath); } OCFile newFile = new OCFile(remotePath); newFile.setStoragePath(localPath); newFile.setLastSyncDateForProperties(0); newFile.setLastSyncDateForData(0); // size if (localPath != null && localPath.length() > 0) { File localFile = new File(localPath); newFile.setFileLength(localFile.length()); newFile.setLastSyncDateForData(localFile.lastModified()); } // don't worry about not assigning size, the problems with localPath // are checked when the UploadFileOperation instance is created newFile.setMimetype(mimeType); return newFile; } private static final String TAG = UploadFileOperation.class.getSimpleName(); private Account mAccount; /** * OCFile which is to be uploaded. */ private OCFile mFile; /** * Original OCFile which is to be uploaded in case file had to be renamed * (if forceOverwrite==false and remote file already exists). */ private OCFile mOldFile; private String mRemotePath = null; private boolean mChunked = false; private boolean mRemoteFolderToBeCreated = false; private boolean mForceOverwrite = false; private int mLocalBehaviour = FileUploader.LOCAL_BEHAVIOUR_COPY; private int mCreatedBy = CREATED_BY_USER; private boolean mWasRenamed = false; private long mOCUploadId = -1; /** * Local path to file which is to be uploaded (before any possible renaming or moving). */ private String mOriginalStoragePath = null; private Set mDataTransferListeners = new HashSet(); private OnRenameListener mRenameUploadListener; private final AtomicBoolean mCancellationRequested = new AtomicBoolean(false); private final AtomicBoolean mUploadStarted = new AtomicBoolean(false); private Context mContext; private UploadRemoteFileOperation mUploadOperation; protected RequestEntity mEntity = null; public UploadFileOperation(Account account, OCFile file, OCUpload upload, boolean chunked, boolean forceOverwrite, int localBehaviour, Context context ) { if (account == null) throw new IllegalArgumentException("Illegal NULL account in UploadFileOperation " + "creation"); if (upload == null) throw new IllegalArgumentException("Illegal NULL file in UploadFileOperation creation"); if (upload.getLocalPath() == null || upload.getLocalPath().length() <= 0) { throw new IllegalArgumentException( "Illegal file in UploadFileOperation; storage path invalid: " + upload.getLocalPath()); } mAccount = account; if (file == null) { mFile = obtainNewOCFileToUpload( upload.getRemotePath(), upload.getLocalPath(), upload.getMimeType() ); } else { mFile = file; } mRemotePath = upload.getRemotePath(); mChunked = chunked; mForceOverwrite = forceOverwrite; mLocalBehaviour = localBehaviour; mOriginalStoragePath = mFile.getStoragePath(); mContext = context; mOCUploadId = upload.getUploadId(); mCreatedBy = upload.getCreadtedBy(); mRemoteFolderToBeCreated = upload.isCreateRemoteFolder(); } public Account getAccount() { return mAccount; } public String getFileName() { return (mFile != null) ? mFile.getFileName() : null; } public OCFile getFile() { return mFile; } /** * If remote file was renamed, return original OCFile which was uploaded. Is * null is file was not renamed. */ public OCFile getOldFile() { return mOldFile; } public String getOriginalStoragePath() { return mOriginalStoragePath; } public String getStoragePath() { return mFile.getStoragePath(); } public String getRemotePath() { return mFile.getRemotePath(); } public String getMimeType() { return mFile.getMimetype(); } public int getLocalBehaviour() { return mLocalBehaviour; } public void setRemoteFolderToBeCreated() { mRemoteFolderToBeCreated = true; } public boolean wasRenamed() { return mWasRenamed; } 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 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); } } public void addRenameUploadListener (OnRenameListener listener) { mRenameUploadListener = listener; } @Override protected RemoteOperationResult run(OwnCloudClient client) { mCancellationRequested.set(false); mUploadStarted.set(true); RemoteOperationResult result = null; File temporalFile = null, originalFile = new File(mOriginalStoragePath), expectedFile = null; try { /// Check that connectivity conditions are met and delays the upload otherwise if (delayForWifi()) { Log_OC.d(TAG, "Upload delayed until WiFi is available: " + getRemotePath()); return new RemoteOperationResult(ResultCode.DELAYED_FOR_WIFI); } /// check if the file continues existing before schedule the operation if (!originalFile.exists()) { Log_OC.d(TAG, mOriginalStoragePath.toString() + " not exists anymore"); return new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND); } /// 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; } /// set parent local id in uploading file OCFile parent = getStorageManager().getFileByPath(remoteParentPath); mFile.setParentId(parent.getFileId()); /// automatic rename of file to upload in case of name collision in server Log_OC.d(TAG, "Checking name collision in server"); if (!mForceOverwrite) { String remotePath = getAvailableRemotePath(client, mRemotePath); mWasRenamed = !remotePath.equals(mRemotePath); if (mWasRenamed) { createNewOCFile(remotePath); Log_OC.d(TAG, "File renamed as " + remotePath); } mRemotePath = remotePath; mRenameUploadListener.onRenameUpload(); } if (mCancellationRequested.get()) { throw new OperationCancelledException(); } String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, mFile); expectedFile = new File(expectedPath); /// copy the file locally before uploading if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_COPY && !mOriginalStoragePath.equals(expectedPath)) { String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + mFile.getRemotePath(); mFile.setStoragePath(temporalPath); temporalFile = new File(temporalPath); result = copy(originalFile, temporalFile); if (result != null) { return result; } } if (mCancellationRequested.get()) { throw new OperationCancelledException(); } /// perform the upload if ( mChunked && (new File(mFile.getStoragePath())).length() > ChunkedUploadRemoteFileOperation.CHUNK_SIZE ) { mUploadOperation = new ChunkedUploadRemoteFileOperation(mFile.getStoragePath(), mFile.getRemotePath(), mFile.getMimetype(), mFile.getEtagInConflict()); } else { mUploadOperation = new UploadRemoteFileOperation(mFile.getStoragePath(), mFile.getRemotePath(), mFile.getMimetype(), mFile.getEtagInConflict()); } Iterator listener = mDataTransferListeners.iterator(); while (listener.hasNext()) { mUploadOperation.addDatatransferProgressListener(listener.next()); } if (mCancellationRequested.get()) { throw new OperationCancelledException(); } result = mUploadOperation.execute(client); /// move local temporal file or original file to its corresponding // location in the ownCloud local folder if (result.isSuccess()) { if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_FORGET) { String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + mFile.getRemotePath(); if (mOriginalStoragePath.equals(temporalPath)) { // delete local file is was pre-copied in temporary folder (see .ui.helpers.UriUploader) temporalFile = new File(temporalPath); temporalFile.delete(); } mFile.setStoragePath(""); } else { mFile.setStoragePath(expectedPath); if (temporalFile != null) { // FileUploader.LOCAL_BEHAVIOUR_COPY move(temporalFile, expectedFile); } else { // FileUploader.LOCAL_BEHAVIOUR_MOVE move(originalFile, expectedFile); getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath()); } FileDataStorageManager.triggerMediaScan(expectedFile.getAbsolutePath()); } } else if (result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED ) { result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); } } catch (Exception e) { result = new RemoteOperationResult(e); } finally { mUploadStarted.set(false); 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 " + mOriginalStoragePath + " to " + mRemotePath + ": " + result.getLogMessage()); } else { if (result.getException() != null) { if(result.isCancelled()){ Log_OC.w(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + ": " + result.getLogMessage()); } else { Log_OC.e(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + ": " + result.getLogMessage(), result.getException()); } } else { Log_OC.e(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + ": " + result.getLogMessage()); } } } if (result.isSuccess()) { saveUploadedFile(client); } else if (result.getCode() == ResultCode.SYNC_CONFLICT) { getStorageManager().saveConflict(mFile, mFile.getEtagInConflict()); } return result; } /** * Checks origin of current upload and network type to decide if should be delayed, according to * current user preferences. * * @return 'True' if the upload was delayed until WiFi connectivity is available, 'false' otherwise. */ private boolean delayForWifi() { boolean delayInstantPicture = ( isInstantPicture() && PreferenceManager.instantPictureUploadViaWiFiOnly(mContext) ); boolean delayInstantVideo = ( isInstantVideo() && PreferenceManager.instantVideoUploadViaWiFiOnly(mContext) ); return ( (delayInstantPicture || delayInstantVideo) && !ConnectivityUtils.isAppConnectedViaUnmeteredWiFi(mContext) ); } /** * Checks the existence of the folder where the current file will be uploaded both * in the remote server and in the local database. *

* 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, mContext, 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; } /** * Create a new OCFile mFile with new remote path. This is required if forceOverwrite==false. * New file is stored as mFile, original as mOldFile. * @param newRemotePath new remote path */ private void createNewOCFile(String newRemotePath) { // a new OCFile instance must be created for a new remote path OCFile newFile = new OCFile(newRemotePath); newFile.setCreationTimestamp(mFile.getCreationTimestamp()); newFile.setFileLength(mFile.getFileLength()); newFile.setMimetype(mFile.getMimetype()); newFile.setModificationTimestamp(mFile.getModificationTimestamp()); newFile.setModificationTimestampAtLastSyncForData( mFile.getModificationTimestampAtLastSyncForData() ); newFile.setEtag(mFile.getEtag()); newFile.setFavorite(mFile.isFavorite()); newFile.setLastSyncDateForProperties(mFile.getLastSyncDateForProperties()); newFile.setLastSyncDateForData(mFile.getLastSyncDateForData()); newFile.setStoragePath(mFile.getStoragePath()); newFile.setParentId(mFile.getParentId()); mOldFile = mFile; mFile = newFile; } /** * 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, mContext, 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(mAccount.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 = mContext.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 * * 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){ mFile.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 { mFile.setStoragePath(""); } } } /** * Saves a OC File after a successful upload. *

* A PROPFIND is necessary to keep the props in the local database * synchronized with the server, specially the modification time and Etag * (where available) *

*/ private void saveUploadedFile(OwnCloudClient client) { OCFile file = mFile; 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..."); } if (mWasRenamed) { OCFile oldFile = getStorageManager().getFileByPath(mOldFile.getRemotePath()); if (oldFile != null) { oldFile.setStoragePath(null); getStorageManager().saveFile(oldFile); getStorageManager().saveConflict(oldFile, null); } // else: it was just an automatic renaming due to a name // coincidence; nothing else is needed, the storagePath is right // in the instance returned by mCurrentUpload.getFile() } file.setNeedsUpdateThumbnail(true); getStorageManager().saveFile(file); getStorageManager().saveConflict(file, null); FileDataStorageManager.triggerMediaScan(file.getStoragePath()); // generate new Thumbnail final ThumbnailsCacheManager.ThumbnailGenerationTask task = new ThumbnailsCacheManager.ThumbnailGenerationTask(getStorageManager(), mAccount); task.execute(file); } private void updateOCFile(OCFile file, RemoteFile remoteFile) { file.setCreationTimestamp(remoteFile.getCreationTimestamp()); file.setFileLength(remoteFile.getLength()); file.setMimetype(remoteFile.getMimeType()); file.setModificationTimestamp(remoteFile.getModifiedTimestamp()); file.setModificationTimestampAtLastSyncForData(remoteFile.getModifiedTimestamp()); file.setEtag(remoteFile.getEtag()); file.setRemoteId(remoteFile.getRemoteId()); } public interface OnRenameListener { void onRenameUpload(); } }