along with this program. If not, see <http://www.gnu.org/licenses/>.\r
-->\r
<manifest package="com.owncloud.android"\r
- android:versionCode="103009"\r
- android:versionName="1.3.9" xmlns:android="http://schemas.android.com/apk/res/android">\r
+ android:versionCode="103010"\r
+ android:versionName="1.3.10" xmlns:android="http://schemas.android.com/apk/res/android">\r
\r
<uses-permission android:name="android.permission.GET_ACCOUNTS" />\r
<uses-permission android:name="android.permission.USE_CREDENTIALS" />\r
/** 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;
return mLatestResult;
}
}
-
+
}
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;
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;
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<Account> mAccounts = new Vector<Account>();
+ private AbstractList<UploadFileOperation> mUploads = new Vector<UploadFileOperation>();
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
}
+ /**
+ * 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
@Override
public void handleMessage(Message msg) {
- uploadFile(msg.arg2==1?true:false);
+ uploadFile();
stopSelf(msg.arg1);
}
}
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;
/**
* 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;
+ Iterator<UploadFileOperation> 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");
+
+
+ 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);
+ }
- /// prepare client object to send the request to the ownCloud server
- WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(mAccount, getApplicationContext());
- wc.setDataTransferProgressListener(this);
+ /// 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);
+ }
- /// create status notification to show the upload progress
+ /**
+ * 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);
// 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(),
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;
- }
- }
/**
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);
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
+ }
}
}
}
--- /dev/null
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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;
+ }
+
+}
public interface OnRemoteOperationListener {
void onRemoteOperationFinish(RemoteOperation caller, RemoteOperationResult result);
-
+
}
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
package com.owncloud.android.operations;
+import java.io.IOException;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
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;
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,
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);
- */
+
}
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") + ")";
+
+ }
}
--- /dev/null
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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;
+ }
+ }
+
+}
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
--- /dev/null
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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
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;
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);
import java.io.FileOutputStream;\r
import java.io.IOException;\r
import java.io.InputStream;\r
+import java.util.Random;\r
\r
import org.apache.commons.httpclient.Credentials;\r
import org.apache.commons.httpclient.HttpClient;\r
/**\r
* Creates or update a file in the remote server with the contents of a local file.\r
* \r
- * \r
* @param localFile Path to the local file to upload.\r
* @param remoteTarget Remote path to the file to create or update, URL DECODED\r
* @param contentType MIME type of the file.\r
- * @return 'True' then the upload was successfully completed\r
+ * @return Status HTTP code returned by the server.\r
+ * @throws IOException When a transport error that could not be recovered occurred while uploading the file to the server.\r
+ * @throws HttpException When a violation of the HTTP protocol occurred. \r
*/\r
- public boolean putFile(String localFile, String remoteTarget, String contentType) {\r
- boolean result = false;\r
+ public int putFile(String localFile, String remoteTarget, String contentType) throws HttpException, IOException {\r
int status = -1;\r
PutMethod put = new PutMethod(mUri.toString() + WebdavUtils.encodePath(remoteTarget));\r
\r
put.setRequestEntity(entity);\r
status = executeMethod(put);\r
\r
- result = (status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED || status == HttpStatus.SC_NO_CONTENT);\r
- \r
- Log.d(TAG, "PUT to " + remoteTarget + " finished with HTTP status " + status + (!result?"(FAIL)":""));\r
-\r
exhaustResponse(put.getResponseBodyAsStream());\r
\r
- } catch (Exception e) {\r
- logException(e, "uploading " + localFile + " to " + remoteTarget);\r
- \r
} finally {\r
put.releaseConnection(); // let the connection available for other methods\r
}\r
- return result;\r
+ return status;\r
}\r
-\r
+ \r
/**\r
* Tries to log in to the current URI, with the current credentials\r
* \r
/**\r
* Check if a file exists in the OC server\r
* \r
- * @return 'Boolean.TRUE' if the file exists; 'Boolean.FALSE' it doesn't exist; NULL if couldn't be checked\r
+ * @return 'true' if the file exists; 'false' it doesn't exist\r
+ * @throws Exception When the existence could not be determined\r
*/\r
- public Boolean existsFile(String path) {\r
+ public boolean existsFile(String path) throws IOException, HttpException {\r
HeadMethod head = new HeadMethod(mUri.toString() + WebdavUtils.encodePath(path));\r
try {\r
int status = executeMethod(head);\r
exhaustResponse(head.getResponseBodyAsStream());\r
return (status == HttpStatus.SC_OK);\r
\r
- } catch (Exception e) {\r
- logException(e, "checking existence of " + path);\r
- return null;\r
- \r
} finally {\r
head.releaseConnection(); // let the connection available for other methods\r
}\r
* \r
* @param responseBodyAsStream InputStream with the HTTP response to exhaust.\r
*/\r
- private static void exhaustResponse(InputStream responseBodyAsStream) {\r
+ public void exhaustResponse(InputStream responseBodyAsStream) {\r
if (responseBodyAsStream != null) {\r
try {\r
while (responseBodyAsStream.read(sExhaustBuffer) >= 0);\r
public void setBaseUri(Uri uri) {\r
mUri = uri;\r
}\r
+\r
+ public Uri getBaseUri() {\r
+ return mUri;\r
+ }\r
\r
}\r