From 53b67429ea8c97b83602cec3446a10e0b6b7ff5e Mon Sep 17 00:00:00 2001 From: "David A. Velasco" Date: Wed, 12 Sep 2012 16:16:56 +0200 Subject: [PATCH] Added support for OC-Chunking in uploads (disabled, but ready) --- AndroidManifest.xml | 4 +- .../ConnectionCheckOperation.java | 4 +- .../android/files/services/FileUploader.java | 349 ++++++++++-------- .../files/services/InstantUploadService.java | 6 +- .../ChunkedUploadFileOperation.java | 89 +++++ .../operations/OnRemoteOperationListener.java | 2 +- .../operations/RemoteOperationResult.java | 82 +++- .../operations/UploadFileOperation.java | 195 ++++++++++ .../android/utils/OwnCloudVersion.java | 2 + .../ChunkFromFileChannelRequestEntity.java | 109 ++++++ src/eu/alefzero/webdav/FileRequestEntity.java | 5 +- src/eu/alefzero/webdav/WebdavClient.java | 35 +- 12 files changed, 681 insertions(+), 201 deletions(-) create mode 100644 src/com/owncloud/android/operations/ChunkedUploadFileOperation.java create mode 100644 src/com/owncloud/android/operations/UploadFileOperation.java create mode 100644 src/eu/alefzero/webdav/ChunkFromFileChannelRequestEntity.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 3d0bc2e21c..7798df1781 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -17,8 +17,8 @@ along with this program. If not, see . --> + android:versionCode="103010" + android:versionName="1.3.10" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/src/com/owncloud/android/authenticator/ConnectionCheckOperation.java b/src/com/owncloud/android/authenticator/ConnectionCheckOperation.java index 17b0dc2317..5410dec457 100644 --- a/src/com/owncloud/android/authenticator/ConnectionCheckOperation.java +++ b/src/com/owncloud/android/authenticator/ConnectionCheckOperation.java @@ -39,7 +39,7 @@ public class ConnectionCheckOperation extends RemoteOperation { /** Maximum time to wait for a response from the server when the connection is being tested, in MILLISECONDs. */ public static final int TRY_CONNECTION_TIMEOUT = 5000; - private static final String TAG = ConnectionCheckerRunnable.class.getCanonicalName(); + private static final String TAG = ConnectionCheckOperation.class.getCanonicalName(); private String mUrl; private RemoteOperationResult mLatestResult; @@ -132,5 +132,5 @@ public class ConnectionCheckOperation extends RemoteOperation { return mLatestResult; } } - + } diff --git a/src/com/owncloud/android/files/services/FileUploader.java b/src/com/owncloud/android/files/services/FileUploader.java index e66181bee3..6cf7a5cfcf 100644 --- a/src/com/owncloud/android/files/services/FileUploader.java +++ b/src/com/owncloud/android/files/services/FileUploader.java @@ -1,19 +1,29 @@ package com.owncloud.android.files.services; import java.io.File; +import java.util.AbstractList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; +import java.util.Vector; +import com.owncloud.android.authenticator.AccountAuthenticator; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.files.PhotoTakenBroadcastReceiver; +import com.owncloud.android.operations.ChunkedUploadFileOperation; +import com.owncloud.android.operations.RemoteOperationResult; +import com.owncloud.android.operations.UploadFileOperation; +import com.owncloud.android.utils.OwnCloudVersion; +import eu.alefzero.webdav.ChunkFromFileChannelRequestEntity; import eu.alefzero.webdav.OnDatatransferProgressListener; import com.owncloud.android.network.OwnCloudClientUtils; import android.accounts.Account; +import android.accounts.AccountManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -26,8 +36,9 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.util.Log; -import android.webkit.MimeTypeMap; import android.widget.RemoteViews; + +import com.owncloud.android.AccountUtils; import com.owncloud.android.R; import eu.alefzero.webdav.WebdavClient; @@ -51,19 +62,19 @@ public class FileUploader extends Service implements OnDatatransferProgressListe public static final int UPLOAD_SINGLE_FILE = 0; public static final int UPLOAD_MULTIPLE_FILES = 1; - private static final String TAG = "FileUploader"; + private static final String TAG = FileUploader.class.getSimpleName(); private NotificationManager mNotificationManager; private Looper mServiceLooper; private ServiceHandler mServiceHandler; - private Account mAccount; - private String[] mLocalPaths, mRemotePaths, mMimeTypes; - private int mUploadType; + private AbstractList mAccounts = new Vector(); + private AbstractList mUploads = new Vector(); private Notification mNotification; private long mTotalDataToSend, mSendData; + private int mTotalFilesToSend; private int mCurrentIndexUpload, mPreviousPercent; private int mSuccessCounter; - private boolean mIsInstant; + private RemoteViews mDefaultNotificationContentView; /** * Static map with the files being download and the path to the temporal file were are download @@ -85,6 +96,17 @@ public class FileUploader extends Service implements OnDatatransferProgressListe } + /** + * Checks if an ownCloud server version should support chunked uploads. + * + * @param version OwnCloud version instance corresponding to an ownCloud server. + * @return 'True' if the ownCloud server with version supports chunked uploads. + */ + private static boolean chunkedUploadIsSupported(OwnCloudVersion version) { + //return (version != null && version.compareTo(OwnCloudVersion.owncloud_v4_5) >= 0); // TODO uncomment when feature is full in server + return false; + } + @Override @@ -99,7 +121,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe @Override public void handleMessage(Message msg) { - uploadFile(msg.arg2==1?true:false); + uploadFile(); stopSelf(msg.arg1); } } @@ -121,34 +143,50 @@ public class FileUploader extends Service implements OnDatatransferProgressListe Log.e(TAG, "Not enough information provided in intent"); return Service.START_NOT_STICKY; } - mAccount = intent.getParcelableExtra(KEY_ACCOUNT); - mUploadType = intent.getIntExtra(KEY_UPLOAD_TYPE, -1); - if (mUploadType == -1) { + Account account = intent.getParcelableExtra(KEY_ACCOUNT); + if (account == null) { + Log.e(TAG, "Bad account information provided in upload intent"); + return Service.START_NOT_STICKY; + } + + int uploadType = intent.getIntExtra(KEY_UPLOAD_TYPE, -1); + if (uploadType == -1) { Log.e(TAG, "Incorrect upload type provided"); return Service.START_NOT_STICKY; } - if (mUploadType == UPLOAD_SINGLE_FILE) { - mLocalPaths = new String[] { intent.getStringExtra(KEY_LOCAL_FILE) }; - mRemotePaths = new String[] { intent + String[] localPaths, remotePaths, mimeTypes; + if (uploadType == UPLOAD_SINGLE_FILE) { + localPaths = new String[] { intent.getStringExtra(KEY_LOCAL_FILE) }; + remotePaths = new String[] { intent .getStringExtra(KEY_REMOTE_FILE) }; - mMimeTypes = new String[] { intent.getStringExtra(KEY_MIME_TYPE) }; + mimeTypes = new String[] { intent.getStringExtra(KEY_MIME_TYPE) }; } else { // mUploadType == UPLOAD_MULTIPLE_FILES - mLocalPaths = intent.getStringArrayExtra(KEY_LOCAL_FILE); - mRemotePaths = intent.getStringArrayExtra(KEY_REMOTE_FILE); - mMimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE); + localPaths = intent.getStringArrayExtra(KEY_LOCAL_FILE); + remotePaths = intent.getStringArrayExtra(KEY_REMOTE_FILE); + mimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE); } - if (mLocalPaths.length != mRemotePaths.length) { + if (localPaths.length != remotePaths.length) { Log.e(TAG, "Different number of remote paths and local paths!"); return Service.START_NOT_STICKY; } + + boolean isInstant = intent.getBooleanExtra(KEY_INSTANT_UPLOAD, false); + boolean forceOverwrite = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false); + + for (int i=0; i < localPaths.length; i++) { + OwnCloudVersion ocv = new OwnCloudVersion(AccountManager.get(this).getUserData(account, AccountAuthenticator.KEY_OC_VERSION)); + if (FileUploader.chunkedUploadIsSupported(ocv)) { + mUploads.add(new ChunkedUploadFileOperation(localPaths[i], remotePaths[i], ((mimeTypes!=null)?mimeTypes[i]:""), isInstant, forceOverwrite, this)); + } else { + mUploads.add(new UploadFileOperation(localPaths[i], remotePaths[i], (mimeTypes!=null?mimeTypes[i]:""), isInstant, forceOverwrite, this)); + } + mAccounts.add(account); + } - mIsInstant = intent.getBooleanExtra(KEY_INSTANT_UPLOAD, false); - Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; - msg.arg2 = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false)?1:0; mServiceHandler.sendMessage(msg); return Service.START_NOT_STICKY; @@ -158,19 +196,117 @@ public class FileUploader extends Service implements OnDatatransferProgressListe /** * Core upload method: sends the file(s) to upload */ - public void uploadFile(boolean force_override) { - FileDataStorageManager storageManager = new FileDataStorageManager(mAccount, getContentResolver()); + public void uploadFile() { + /// prepare upload statistics mTotalDataToSend = mSendData = mPreviousPercent = 0; - - /// prepare client object to send the request to the ownCloud server - WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(mAccount, getApplicationContext()); - wc.setDataTransferProgressListener(this); + Iterator it = mUploads.iterator(); + while (it.hasNext()) { + mTotalDataToSend += new File(it.next().getLocalPath()).length(); + } + mTotalFilesToSend = mUploads.size(); + Log.d(TAG, "Will upload " + mTotalDataToSend + " bytes, with " + mUploads.size() + " files"); - /// create status notification to show the upload progress + + notifyUploadStart(); + + UploadFileOperation currentUpload; + Account currentAccount, lastAccount = null; + FileDataStorageManager storageManager = null; + WebdavClient wc = null; + mSuccessCounter = 0; + boolean createdInstantDir = false; + + for (mCurrentIndexUpload = 0; mCurrentIndexUpload < mUploads.size(); mCurrentIndexUpload++) { + currentUpload = mUploads.get(mCurrentIndexUpload); + currentAccount = mAccounts.get(mCurrentIndexUpload); + + /// prepare client object to send request(s) to the ownCloud server + if (lastAccount == null || !lastAccount.equals(currentAccount)) { + storageManager = new FileDataStorageManager(currentAccount, getContentResolver()); + wc = OwnCloudClientUtils.createOwnCloudClient(currentAccount, getApplicationContext()); + wc.setDataTransferProgressListener(this); + } + + if (currentUpload.isInstant() && !createdInstantDir) { + createdInstantDir = createRemoteFolderForInstantUploads(wc, storageManager); + } + + /// perform the upload + long parentDirId = -1; + RemoteOperationResult uploadResult = null; + boolean updateResult = false; + try { + File remote = new File(currentUpload.getRemotePath()); + parentDirId = storageManager.getFileByPath(remote.getParent().endsWith("/")?remote.getParent():remote.getParent()+"/").getFileId(); + File local = new File(currentUpload.getLocalPath()); + long size = local.length(); + mUploadsInProgress.put(buildRemoteName(currentAccount.name, currentUpload.getRemotePath()), currentUpload.getLocalPath()); + uploadResult = currentUpload.execute(wc); + if (uploadResult.isSuccess()) { + saveNewOCFile(currentUpload, storageManager, parentDirId, size); + mSuccessCounter++; + updateResult = true; + } + + } finally { + mUploadsInProgress.remove(buildRemoteName(currentAccount.name, currentUpload.getRemotePath())); + broadcastUploadEnd(currentUpload, currentAccount, updateResult, parentDirId); + } + } + + notifyUploadEndOverview(); + + } + + /** + * Create remote folder for instant uploads if necessary. + * + * @param client WebdavClient to the ownCloud server. + * @param storageManager Interface to the local database caching the data in the server. + * @return 'True' if the folder exists when the methods finishes. + */ + private boolean createRemoteFolderForInstantUploads(WebdavClient client, FileDataStorageManager storageManager) { + boolean result = true; + OCFile instantUploadDir = storageManager.getFileByPath(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR); + if (instantUploadDir == null) { + result = client.createDirectory(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR); // fail could just mean that it already exists, but local database is not synchronized; the upload will be started anyway + OCFile newDir = new OCFile(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR); + newDir.setMimetype("DIR"); + newDir.setParentId(storageManager.getFileByPath(OCFile.PATH_SEPARATOR).getFileId()); + storageManager.saveFile(newDir); + } + return result; + } + + /** + * Saves a new OC File after a successful upload. + * + * @param upload Upload operation completed. + * @param storageManager Interface to the database where the new OCFile has to be stored. + * @param parentDirId Id of the parent OCFile. + * @param size Size of the file. + */ + private void saveNewOCFile(UploadFileOperation upload, FileDataStorageManager storageManager, long parentDirId, long size) { + OCFile newFile = new OCFile(upload.getRemotePath()); + newFile.setMimetype(upload.getMimeType()); + newFile.setFileLength(size); + newFile.setModificationTimestamp(System.currentTimeMillis()); + newFile.setLastSyncDate(0); + newFile.setStoragePath(upload.getLocalPath()); + newFile.setParentId(parentDirId); + if (upload.getForceOverwrite()) + newFile.setKeepInSync(true); + storageManager.saveFile(newFile); + } + + /** + * Creates a status notification to show the upload progress + */ + private void notifyUploadStart() { mNotification = new Notification(R.drawable.icon, getString(R.string.uploader_upload_in_progress_ticker), System.currentTimeMillis()); mNotification.flags |= Notification.FLAG_ONGOING_EVENT; - RemoteViews oldContentView = mNotification.contentView; + mDefaultNotificationContentView = mNotification.contentView; mNotification.contentView = new RemoteViews(getApplicationContext().getPackageName(), R.layout.progressbar_layout); mNotification.contentView.setProgressBar(R.id.status_progress, 100, 0, false); mNotification.contentView.setImageViewResource(R.id.status_icon, R.drawable.icon); @@ -178,96 +314,39 @@ public class FileUploader extends Service implements OnDatatransferProgressListe // BUT an empty Intent is not a very elegant solution; something smart should happen when a user 'clicks' on an upload in the notification bar mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT); mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotification); + } - /// create remote folder for instant uploads if necessary - if (mIsInstant) { - OCFile instantUploadDir = storageManager.getFileByPath(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR); - if (instantUploadDir == null) { - wc.createDirectory(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR); // fail could just mean that it already exists, but local database is not synchronized; the upload will be started anyway - OCFile newDir = new OCFile(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR); - newDir.setMimetype("DIR"); - newDir.setParentId(storageManager.getFileByPath(OCFile.PATH_SEPARATOR).getFileId()); - storageManager.saveFile(newDir); - } - } - - /// perform the upload - File [] localFiles = new File[mLocalPaths.length]; - for (int i = 0; i < mLocalPaths.length; ++i) { - localFiles[i] = new File(mLocalPaths[i]); - mTotalDataToSend += localFiles[i].length(); - } - Log.d(TAG, "Will upload " + mTotalDataToSend + " bytes, with " + mLocalPaths.length + " files"); - mSuccessCounter = 0; - for (int i = 0; i < mLocalPaths.length; ++i) { - String mimeType = (mMimeTypes != null) ? mMimeTypes[i] : null; - if (mimeType == null) { - try { - mimeType = MimeTypeMap.getSingleton() - .getMimeTypeFromExtension( - mLocalPaths[i].substring(mLocalPaths[i] - .lastIndexOf('.') + 1)); - } catch (IndexOutOfBoundsException e) { - Log.e(TAG, "Trying to find out MIME type of a file without extension: " + mLocalPaths[i]); - } - } - if (mimeType == null) - mimeType = "application/octet-stream"; - mCurrentIndexUpload = i; - long parentDirId = -1; - boolean uploadResult = false; - String availablePath = mRemotePaths[i]; - if (!force_override) - availablePath = getAvailableRemotePath(wc, mRemotePaths[i]); - try { - File f = new File(mRemotePaths[i]); - long size = localFiles[i].length(); - parentDirId = storageManager.getFileByPath(f.getParent().endsWith("/")?f.getParent():f.getParent()+"/").getFileId(); - if(availablePath != null) { - mRemotePaths[i] = availablePath; - mUploadsInProgress.put(buildRemoteName(mAccount.name, mRemotePaths[i]), mLocalPaths[i]); - if (wc.putFile(mLocalPaths[i], mRemotePaths[i], mimeType)) { - OCFile new_file = new OCFile(mRemotePaths[i]); - new_file.setMimetype(mimeType); - new_file.setFileLength(size); - new_file.setModificationTimestamp(System.currentTimeMillis()); - new_file.setLastSyncDate(0); - new_file.setStoragePath(mLocalPaths[i]); - new_file.setParentId(parentDirId); - if (force_override) - new_file.setKeepInSync(true); - storageManager.saveFile(new_file); - mSuccessCounter++; - uploadResult = true; - } - } - } finally { - mUploadsInProgress.remove(buildRemoteName(mAccount.name, mRemotePaths[i])); - - /// notify upload (or fail) of EACH file to activities interested - Intent end = new Intent(UPLOAD_FINISH_MESSAGE); - end.putExtra(EXTRA_PARENT_DIR_ID, parentDirId); - end.putExtra(EXTRA_UPLOAD_RESULT, uploadResult); - end.putExtra(EXTRA_REMOTE_PATH, mRemotePaths[i]); - end.putExtra(EXTRA_FILE_PATH, mLocalPaths[i]); - end.putExtra(ACCOUNT_NAME, mAccount.name); - sendBroadcast(end); - } - - } - + + /** + * Notifies upload (or fail) of a file to activities interested + */ + private void broadcastUploadEnd(UploadFileOperation upload, Account account, boolean success, long parentDirId) { + /// + Intent end = new Intent(UPLOAD_FINISH_MESSAGE); + end.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath()); + end.putExtra(EXTRA_FILE_PATH, upload.getLocalPath()); + end.putExtra(ACCOUNT_NAME, account.name); + end.putExtra(EXTRA_UPLOAD_RESULT, success); + end.putExtra(EXTRA_PARENT_DIR_ID, parentDirId); + sendBroadcast(end); + } + + + /** + * Updates the status notification with the results of a batch of uploads. + */ + private void notifyUploadEndOverview() { /// notify final result - if (mSuccessCounter == mLocalPaths.length) { // success - //Notification finalNotification = new Notification(R.drawable.icon, getString(R.string.uploader_upload_succeeded_ticker), System.currentTimeMillis()); + if (mSuccessCounter == mTotalFilesToSend) { // success mNotification.flags ^= Notification.FLAG_ONGOING_EVENT; // remove the ongoing flag mNotification.flags |= Notification.FLAG_AUTO_CANCEL; - mNotification.contentView = oldContentView; + mNotification.contentView = mDefaultNotificationContentView; // TODO put something smart in the contentIntent below mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT); - if (mLocalPaths.length == 1) { + if (mTotalFilesToSend == 1) { mNotification.setLatestEventInfo( getApplicationContext(), getString(R.string.uploader_upload_succeeded_ticker), - String.format(getString(R.string.uploader_upload_succeeded_content_single), localFiles[0].getName()), + String.format(getString(R.string.uploader_upload_succeeded_content_single), (new File(mUploads.get(0).getLocalPath())).getName()), mNotification.contentIntent); } else { mNotification.setLatestEventInfo( getApplicationContext(), @@ -283,61 +362,21 @@ public class FileUploader extends Service implements OnDatatransferProgressListe finalNotification.flags |= Notification.FLAG_AUTO_CANCEL; // TODO put something smart in the contentIntent below finalNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT); - if (mLocalPaths.length == 1) { + if (mTotalFilesToSend == 1) { finalNotification.setLatestEventInfo( getApplicationContext(), getString(R.string.uploader_upload_failed_ticker), - String.format(getString(R.string.uploader_upload_failed_content_single), localFiles[0].getName()), + String.format(getString(R.string.uploader_upload_failed_content_single), (new File(mUploads.get(0).getLocalPath())).getName()), finalNotification.contentIntent); } else { finalNotification.setLatestEventInfo( getApplicationContext(), getString(R.string.uploader_upload_failed_ticker), - String.format(getString(R.string.uploader_upload_failed_content_multiple), mSuccessCounter, mLocalPaths.length), + String.format(getString(R.string.uploader_upload_failed_content_multiple), mSuccessCounter, mTotalFilesToSend), finalNotification.contentIntent); } mNotificationManager.notify(R.string.uploader_upload_failed_ticker, finalNotification); } } - - /** - * 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 string - * @return - */ - private String getAvailableRemotePath(WebdavClient wc, String remotePath) { - Boolean check = wc.existsFile(remotePath); - if (check == null) { // null means fail - return null; - } else 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; - while (check != null && check) { - suffix = " (" + count + ")"; - if (pos >= 0) - check = wc.existsFile(remotePath + suffix + "." + extension); - else - check = wc.existsFile(remotePath + suffix); - count++; - } - if (check == null) { - return null; - } else if (pos >=0) { - return remotePath + suffix + "." + extension; - } else { - return remotePath + suffix; - } - } /** @@ -348,7 +387,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe mSendData += progressRate; int percent = (int)(100*((double)mSendData)/((double)mTotalDataToSend)); if (percent != mPreviousPercent) { - String text = String.format(getString(R.string.uploader_upload_in_progress_content), percent, new File(mLocalPaths[mCurrentIndexUpload]).getName()); + String text = String.format(getString(R.string.uploader_upload_in_progress_content), percent, new File(mUploads.get(mCurrentIndexUpload).getLocalPath()).getName()); mNotification.contentView.setProgressBar(R.id.status_progress, 100, percent, false); mNotification.contentView.setTextViewText(R.id.status_text, text); mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotification); diff --git a/src/com/owncloud/android/files/services/InstantUploadService.java b/src/com/owncloud/android/files/services/InstantUploadService.java index 4b8393d84d..e7d28ffeee 100644 --- a/src/com/owncloud/android/files/services/InstantUploadService.java +++ b/src/com/owncloud/android/files/services/InstantUploadService.java @@ -128,7 +128,11 @@ public class InstantUploadService extends Service { WebdavClient wdc = OwnCloudClientUtils.createOwnCloudClient(account, getApplicationContext()); wdc.createDirectory(INSTANT_UPLOAD_DIR); // fail could just mean that it already exists; put will be tried anyway - wdc.putFile(filepath, INSTANT_UPLOAD_DIR + "/" + filename, mimetype); + try { + wdc.putFile(filepath, INSTANT_UPLOAD_DIR + "/" + filename, mimetype); + } catch (Exception e) { + // nothing to do; this service is deprecated, indeed + } } } } diff --git a/src/com/owncloud/android/operations/ChunkedUploadFileOperation.java b/src/com/owncloud/android/operations/ChunkedUploadFileOperation.java new file mode 100644 index 0000000000..18fc1efeb7 --- /dev/null +++ b/src/com/owncloud/android/operations/ChunkedUploadFileOperation.java @@ -0,0 +1,89 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.util.Random; + +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.methods.PutMethod; + +import eu.alefzero.webdav.ChunkFromFileChannelRequestEntity; +import eu.alefzero.webdav.OnDatatransferProgressListener; +import eu.alefzero.webdav.WebdavClient; +import eu.alefzero.webdav.WebdavUtils; + +public class ChunkedUploadFileOperation extends UploadFileOperation { + + private static final long CHUNK_SIZE = 8192; + private static final String OC_CHUNKED_HEADER = "OC-Chunked"; + + public ChunkedUploadFileOperation( String localPath, + String remotePath, + String mimeType, + boolean isInstant, + boolean forceOverwrite, + OnDatatransferProgressListener dataTransferProgressListener) { + + super(localPath, remotePath, mimeType, isInstant, forceOverwrite, dataTransferProgressListener); + } + + @Override + protected int uploadFile(WebdavClient client) throws HttpException, IOException { + int status = -1; + + PutMethod put = null; + FileChannel channel = null; + FileLock lock = null; + try { + File file = new File(getLocalPath()); + channel = new RandomAccessFile(file, "rw").getChannel(); + lock = channel.tryLock(); + ChunkFromFileChannelRequestEntity entity = new ChunkFromFileChannelRequestEntity(channel, getMimeType(), CHUNK_SIZE); + entity.setOnDatatransferProgressListener(getDataTransferListener()); + long offset = 0; + String uriPrefix = client.getBaseUri() + WebdavUtils.encodePath(getRemotePath()) + "-chunking-" + Math.abs((new Random()).nextInt()) + "-" ; + long chunkCount = (long) Math.ceil((double)file.length() / CHUNK_SIZE); + for (int chunkIndex = 0; chunkIndex < chunkCount ; chunkIndex++, offset += CHUNK_SIZE) { + put = new PutMethod(uriPrefix + chunkIndex + "-" + chunkCount); + put.addRequestHeader(OC_CHUNKED_HEADER, OC_CHUNKED_HEADER); + entity.setOffset(offset); + put.setRequestEntity(entity); + status = client.executeMethod(put); + client.exhaustResponse(put.getResponseBodyAsStream()); + if (!isSuccess(status)) + break; + } + + } finally { + if (lock != null) + lock.release(); + if (channel != null) + channel.close(); + if (put != null) + put.releaseConnection(); // let the connection available for other methods + } + return status; + } + +} diff --git a/src/com/owncloud/android/operations/OnRemoteOperationListener.java b/src/com/owncloud/android/operations/OnRemoteOperationListener.java index 26537fd8e1..313b5b7576 100644 --- a/src/com/owncloud/android/operations/OnRemoteOperationListener.java +++ b/src/com/owncloud/android/operations/OnRemoteOperationListener.java @@ -3,5 +3,5 @@ package com.owncloud.android.operations; public interface OnRemoteOperationListener { void onRemoteOperationFinish(RemoteOperation caller, RemoteOperationResult result); - + } diff --git a/src/com/owncloud/android/operations/RemoteOperationResult.java b/src/com/owncloud/android/operations/RemoteOperationResult.java index 03afda5533..080e0b6454 100644 --- a/src/com/owncloud/android/operations/RemoteOperationResult.java +++ b/src/com/owncloud/android/operations/RemoteOperationResult.java @@ -1,5 +1,24 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 java.io.IOException; import java.net.MalformedURLException; import java.net.SocketException; import java.net.SocketTimeoutException; @@ -8,6 +27,7 @@ import java.net.UnknownHostException; import javax.net.ssl.SSLException; import org.apache.commons.httpclient.ConnectTimeoutException; +import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.HttpStatus; import android.util.Log; @@ -15,9 +35,16 @@ import android.util.Log; import com.owncloud.android.network.CertificateCombinedException; +/** + * The result of a remote operation required to an ownCloud server. + * + * Provides a common classification of resulst for all the application. + * + * @author David A. Velasco + */ public class RemoteOperationResult { - public enum ResultCode { // TODO leave alone our own errors + public enum ResultCode { OK, OK_SSL, OK_NO_SSL, @@ -72,40 +99,26 @@ public class RemoteOperationResult { if (e instanceof SocketException) { mCode = ResultCode.WRONG_CONNECTION; - Log.e(TAG, "Socket exception", e); } else if (e instanceof SocketTimeoutException) { mCode = ResultCode.TIMEOUT; - Log.e(TAG, "Socket timeout exception", e); } else if (e instanceof ConnectTimeoutException) { mCode = ResultCode.TIMEOUT; - Log.e(TAG, "Connect timeout exception", e); } else if (e instanceof MalformedURLException) { mCode = ResultCode.INCORRECT_ADDRESS; - Log.e(TAG, "Malformed URL exception", e); } else if (e instanceof UnknownHostException) { mCode = ResultCode.HOST_NOT_AVAILABLE; - Log.e(TAG, "Unknown host exception", e); } else if (e instanceof SSLException) { mCode = ResultCode.SSL_ERROR; - Log.e(TAG, "SSL exception", e); } else { mCode = ResultCode.UNKNOWN_ERROR; - Log.e(TAG, "Unknown exception", e); } - - /* } catch (HttpException e) { // other specific exceptions from org.apache.commons.httpclient - Log.e(TAG, "HTTP exception while trying connection", e); - } catch (IOException e) { // UnkownsServiceException, and any other transport exceptions that could occur - Log.e(TAG, "I/O exception while trying connection", e); - } catch (Exception e) { - Log.e(TAG, "Unexpected exception while trying connection", e); - */ + } @@ -148,5 +161,42 @@ public class RemoteOperationResult { else return null; } + + + public String getLogMessage() { + + if (mException != null) { + if (mException instanceof SocketException) { + return "Socket exception"; + + } else if (mException instanceof SocketTimeoutException) { + return "Socket timeout exception"; + + } else if (mException instanceof ConnectTimeoutException) { + return "Connect timeout exception"; + + } else if (mException instanceof MalformedURLException) { + return "Malformed URL exception"; + + } else if (mException instanceof UnknownHostException) { + return "Unknown host exception"; + + } else if (mException instanceof SSLException) { + return "SSL exception"; + + } else if (mException instanceof HttpException) { + return "HTTP violation"; + + } else if (mException instanceof IOException) { + return "Unrecovered transport exception"; + + } else { + return "Unexpected exception"; + } + } + + return "Operation finished with HTTP status code " + mHttpCode + " (" + (isSuccess()?"success":"fail") + ")"; + + } } diff --git a/src/com/owncloud/android/operations/UploadFileOperation.java b/src/com/owncloud/android/operations/UploadFileOperation.java new file mode 100644 index 0000000000..38be5f945a --- /dev/null +++ b/src/com/owncloud/android/operations/UploadFileOperation.java @@ -0,0 +1,195 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 java.io.File; +import java.io.IOException; + +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.http.HttpStatus; + +import com.owncloud.android.operations.RemoteOperation; +import com.owncloud.android.operations.RemoteOperationResult; + +import eu.alefzero.webdav.FileRequestEntity; +import eu.alefzero.webdav.OnDatatransferProgressListener; +import eu.alefzero.webdav.WebdavClient; +import eu.alefzero.webdav.WebdavUtils; +import android.util.Log; +import android.webkit.MimeTypeMap; + +/** + * Remote operation performing the upload of a file to an ownCloud server + * + * @author David A. Velasco + */ +public class UploadFileOperation extends RemoteOperation { + + private static final String TAG = UploadFileOperation.class.getCanonicalName(); + + private String mLocalPath = null; + private String mRemotePath = null; + private String mMimeType = null; + private boolean mIsInstant = false; + private boolean mForceOverwrite = false; + private OnDatatransferProgressListener mDataTransferListener = null; + + + public String getLocalPath() { + return mLocalPath; + } + + public String getRemotePath() { + return mRemotePath; + } + + public String getMimeType() { + return mMimeType; + } + + + public boolean isInstant() { + return mIsInstant; + } + + + public boolean getForceOverwrite() { + return mForceOverwrite; + } + + + public OnDatatransferProgressListener getDataTransferListener() { + return mDataTransferListener ; + } + + + public UploadFileOperation( String localPath, + String remotePath, + String mimeType, + boolean isInstant, + boolean forceOverwrite, + OnDatatransferProgressListener dataTransferProgressListener) { + mLocalPath = localPath; + mRemotePath = remotePath; + mMimeType = mimeType; + if (mMimeType == null) { + try { + mMimeType = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension( + localPath.substring(localPath.lastIndexOf('.') + 1)); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "Trying to find out MIME type of a file without extension: " + localPath); + } + } + if (mMimeType == null) { + mMimeType = "application/octet-stream"; + } + mIsInstant = isInstant; + mForceOverwrite = forceOverwrite; + mDataTransferListener = dataTransferProgressListener; + } + + @Override + protected RemoteOperationResult run(WebdavClient client) { + RemoteOperationResult result = null; + boolean nameCheckPassed = false; + try { + /// rename the file to upload, if necessary + if (!mForceOverwrite) { + mRemotePath = getAvailableRemotePath(client, mRemotePath); + } + + /// perform the upload + nameCheckPassed = true; + int status = uploadFile(client); + result = new RemoteOperationResult(isSuccess(status), status); + Log.i(TAG, "Upload of " + mLocalPath + " to " + mRemotePath + ": " + result.getLogMessage()); + + } catch (Exception e) { + result = new RemoteOperationResult(e); + Log.e(TAG, "Upload of " + mLocalPath + " to " + mRemotePath + ": " + result.getLogMessage() + (nameCheckPassed?"":" (while checking file existence in server)"), e); + + } + + return result; + } + + + public boolean isSuccess(int status) { + return ((status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED || status == HttpStatus.SC_NO_CONTENT)); + } + + + protected int uploadFile(WebdavClient client) throws HttpException, IOException { + int status = -1; + PutMethod put = new PutMethod(client.getBaseUri() + WebdavUtils.encodePath(mRemotePath)); + + try { + File f = new File(mLocalPath); + FileRequestEntity entity = new FileRequestEntity(f, mMimeType); + entity.setOnDatatransferProgressListener(mDataTransferListener); + put.setRequestEntity(entity); + status = client.executeMethod(put); + client.exhaustResponse(put.getResponseBodyAsStream()); + + } finally { + put.releaseConnection(); // let the connection available for other methods + } + return status; + } + + /** + * 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 string + * @return + */ + private String getAvailableRemotePath(WebdavClient wc, String remotePath) throws Exception { + boolean check = wc.existsFile(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 = wc.existsFile(remotePath + suffix + "." + extension); + else + check = wc.existsFile(remotePath + suffix); + count++; + } while (check); + + if (pos >=0) { + return remotePath + suffix + "." + extension; + } else { + return remotePath + suffix; + } + } + +} diff --git a/src/com/owncloud/android/utils/OwnCloudVersion.java b/src/com/owncloud/android/utils/OwnCloudVersion.java index b96a9df167..43ea98387a 100644 --- a/src/com/owncloud/android/utils/OwnCloudVersion.java +++ b/src/com/owncloud/android/utils/OwnCloudVersion.java @@ -27,6 +27,8 @@ public class OwnCloudVersion implements Comparable { 0x030000); public static final OwnCloudVersion owncloud_v4 = new OwnCloudVersion( 0x040000); + public static final OwnCloudVersion owncloud_v4_5 = new OwnCloudVersion( + 0x040500); // format is in version // 0xAABBCC diff --git a/src/eu/alefzero/webdav/ChunkFromFileChannelRequestEntity.java b/src/eu/alefzero/webdav/ChunkFromFileChannelRequestEntity.java new file mode 100644 index 0000000000..bcba354130 --- /dev/null +++ b/src/eu/alefzero/webdav/ChunkFromFileChannelRequestEntity.java @@ -0,0 +1,109 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 eu.alefzero.webdav; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import org.apache.commons.httpclient.methods.RequestEntity; + +import eu.alefzero.webdav.OnDatatransferProgressListener; + +import android.util.Log; + + +/** + * A RequestEntity that represents a PIECE of a file. + * + * @author David A. Velasco + */ +public class ChunkFromFileChannelRequestEntity implements RequestEntity { + + private static final String TAG = ChunkFromFileChannelRequestEntity.class.getSimpleName(); + + //private final File mFile; + private final FileChannel mChannel; + private final String mContentType; + private final long mSize; + private long mOffset; + private OnDatatransferProgressListener mListener; + private ByteBuffer mBuffer = ByteBuffer.allocate(4096); + + public ChunkFromFileChannelRequestEntity(final FileChannel channel, final String contentType, long size) { + super(); + if (channel == null) { + throw new IllegalArgumentException("File may not be null"); + } + if (size <= 0) { + throw new IllegalArgumentException("Size must be greater than zero"); + } + mChannel = channel; + mContentType = contentType; + mSize = size; + mOffset = 0; + } + + public void setOffset(long offset) { + mOffset = offset; + } + + public long getContentLength() { + try { + return Math.min(mSize, mChannel.size() - mChannel.position()); + } catch (IOException e) { + return mSize; + } + } + + public String getContentType() { + return mContentType; + } + + public boolean isRepeatable() { + return true; + } + + public void setOnDatatransferProgressListener(OnDatatransferProgressListener listener) { + mListener = listener; + } + + public void writeRequest(final OutputStream out) throws IOException { + int readCount = 0; + + try { + //while ((i = instream.read(tmp)) >= 0) { + mChannel.position(mOffset); + while (mChannel.position() < mOffset + mSize) { + readCount = mChannel.read(mBuffer); + out.write(mBuffer.array(), 0, readCount); + mBuffer.clear(); + if (mListener != null) + mListener.transferProgress(readCount); + } + + } catch (IOException io) { + Log.e(TAG, io.getMessage()); + throw new RuntimeException("Ugly solution to workaround the default policy of retries when the server falls while uploading ; temporal fix; really", io); + + } + } + +} \ No newline at end of file diff --git a/src/eu/alefzero/webdav/FileRequestEntity.java b/src/eu/alefzero/webdav/FileRequestEntity.java index f8ba3204a3..8ebdcbe252 100644 --- a/src/eu/alefzero/webdav/FileRequestEntity.java +++ b/src/eu/alefzero/webdav/FileRequestEntity.java @@ -1,15 +1,12 @@ package eu.alefzero.webdav; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; -import java.nio.channels.OverlappingFileLockException; import org.apache.commons.httpclient.methods.RequestEntity; @@ -52,7 +49,7 @@ public class FileRequestEntity implements RequestEntity { public void setOnDatatransferProgressListener(OnDatatransferProgressListener listener) { this.listener = listener; } - + public void writeRequest(final OutputStream out) throws IOException { //byte[] tmp = new byte[4096]; ByteBuffer tmp = ByteBuffer.allocate(4096); diff --git a/src/eu/alefzero/webdav/WebdavClient.java b/src/eu/alefzero/webdav/WebdavClient.java index ccb08cac56..2e3ef874bb 100644 --- a/src/eu/alefzero/webdav/WebdavClient.java +++ b/src/eu/alefzero/webdav/WebdavClient.java @@ -22,6 +22,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Random; import org.apache.commons.httpclient.Credentials; import org.apache.commons.httpclient.HttpClient; @@ -151,14 +152,14 @@ public class WebdavClient extends HttpClient { /** * Creates or update a file in the remote server with the contents of a local file. * - * * @param localFile Path to the local file to upload. * @param remoteTarget Remote path to the file to create or update, URL DECODED * @param contentType MIME type of the file. - * @return 'True' then the upload was successfully completed + * @return Status HTTP code returned by the server. + * @throws IOException When a transport error that could not be recovered occurred while uploading the file to the server. + * @throws HttpException When a violation of the HTTP protocol occurred. */ - public boolean putFile(String localFile, String remoteTarget, String contentType) { - boolean result = false; + public int putFile(String localFile, String remoteTarget, String contentType) throws HttpException, IOException { int status = -1; PutMethod put = new PutMethod(mUri.toString() + WebdavUtils.encodePath(remoteTarget)); @@ -169,21 +170,14 @@ public class WebdavClient extends HttpClient { put.setRequestEntity(entity); status = executeMethod(put); - result = (status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED || status == HttpStatus.SC_NO_CONTENT); - - Log.d(TAG, "PUT to " + remoteTarget + " finished with HTTP status " + status + (!result?"(FAIL)":"")); - exhaustResponse(put.getResponseBodyAsStream()); - } catch (Exception e) { - logException(e, "uploading " + localFile + " to " + remoteTarget); - } finally { put.releaseConnection(); // let the connection available for other methods } - return result; + return status; } - + /** * Tries to log in to the current URI, with the current credentials * @@ -239,9 +233,10 @@ public class WebdavClient extends HttpClient { /** * Check if a file exists in the OC server * - * @return 'Boolean.TRUE' if the file exists; 'Boolean.FALSE' it doesn't exist; NULL if couldn't be checked + * @return 'true' if the file exists; 'false' it doesn't exist + * @throws Exception When the existence could not be determined */ - public Boolean existsFile(String path) { + public boolean existsFile(String path) throws IOException, HttpException { HeadMethod head = new HeadMethod(mUri.toString() + WebdavUtils.encodePath(path)); try { int status = executeMethod(head); @@ -249,10 +244,6 @@ public class WebdavClient extends HttpClient { exhaustResponse(head.getResponseBodyAsStream()); return (status == HttpStatus.SC_OK); - } catch (Exception e) { - logException(e, "checking existence of " + path); - return null; - } finally { head.releaseConnection(); // let the connection available for other methods } @@ -295,7 +286,7 @@ public class WebdavClient extends HttpClient { * * @param responseBodyAsStream InputStream with the HTTP response to exhaust. */ - private static void exhaustResponse(InputStream responseBodyAsStream) { + public void exhaustResponse(InputStream responseBodyAsStream) { if (responseBodyAsStream != null) { try { while (responseBodyAsStream.read(sExhaustBuffer) >= 0); @@ -342,5 +333,9 @@ public class WebdavClient extends HttpClient { public void setBaseUri(Uri uri) { mUri = uri; } + + public Uri getBaseUri() { + return mUri; + } }