ac276284fd356c7e42b1281b5a39684e50e1b23d
[pub/Android/ownCloud.git] / src / com / owncloud / android / operations / UploadFileOperation.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012 Bartek Przybylski
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 */
18
19 package com.owncloud.android.operations;
20
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.util.HashSet;
28 import java.util.Set;
29 import java.util.concurrent.atomic.AtomicBoolean;
30
31 import org.apache.commons.httpclient.HttpException;
32 import org.apache.commons.httpclient.methods.PutMethod;
33 import org.apache.http.HttpStatus;
34
35 import com.owncloud.android.datamodel.OCFile;
36 import com.owncloud.android.files.services.FileUploader;
37 import com.owncloud.android.operations.RemoteOperation;
38 import com.owncloud.android.operations.RemoteOperationResult;
39 import com.owncloud.android.operations.RemoteOperationResult.ResultCode;
40 import com.owncloud.android.utils.FileStorageUtils;
41
42 import eu.alefzero.webdav.FileRequestEntity;
43 import eu.alefzero.webdav.OnDatatransferProgressListener;
44 import eu.alefzero.webdav.WebdavClient;
45 import eu.alefzero.webdav.WebdavUtils;
46 import android.accounts.Account;
47 import android.util.Log;
48
49 /**
50 * Remote operation performing the upload of a file to an ownCloud server
51 *
52 * @author David A. Velasco
53 */
54 public class UploadFileOperation extends RemoteOperation {
55
56 private static final String TAG = UploadFileOperation.class.getSimpleName();
57
58 private Account mAccount;
59 private OCFile mFile;
60 private OCFile mOldFile;
61 private String mRemotePath = null;
62 private boolean mIsInstant = false;
63 private boolean mRemoteFolderToBeCreated = false;
64 private boolean mForceOverwrite = false;
65 private int mLocalBehaviour = FileUploader.LOCAL_BEHAVIOUR_COPY;
66 private boolean mWasRenamed = false;
67 private String mOriginalFileName = null;
68 private String mOriginalStoragePath = null;
69 PutMethod mPutMethod = null;
70 private Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<OnDatatransferProgressListener>();
71 private final AtomicBoolean mCancellationRequested = new AtomicBoolean(false);
72
73
74 public UploadFileOperation( Account account,
75 OCFile file,
76 boolean isInstant,
77 boolean forceOverwrite,
78 int localBehaviour) {
79 if (account == null)
80 throw new IllegalArgumentException("Illegal NULL account in UploadFileOperation creation");
81 if (file == null)
82 throw new IllegalArgumentException("Illegal NULL file in UploadFileOperation creation");
83 if (file.getStoragePath() == null || file.getStoragePath().length() <= 0 || !(new File(file.getStoragePath()).exists())) {
84 throw new IllegalArgumentException("Illegal file in UploadFileOperation; storage path invalid or file not found: " + file.getStoragePath());
85 }
86
87 mAccount = account;
88 mFile = file;
89 mRemotePath = file.getRemotePath();
90 mIsInstant = isInstant;
91 mForceOverwrite = forceOverwrite;
92 mLocalBehaviour = localBehaviour;
93 mOriginalStoragePath = mFile.getStoragePath();
94 mOriginalFileName = mFile.getFileName();
95 }
96
97
98 public Account getAccount() {
99 return mAccount;
100 }
101
102 public String getFileName() {
103 return mOriginalFileName;
104 }
105
106 public OCFile getFile() {
107 return mFile;
108 }
109
110 public OCFile getOldFile() {
111 return mOldFile;
112 }
113
114 public String getOriginalStoragePath() {
115 return mOriginalStoragePath;
116 }
117
118 public String getStoragePath() {
119 return mFile.getStoragePath();
120 }
121
122 public String getRemotePath() {
123 return mFile.getRemotePath();
124 }
125
126 public String getMimeType() {
127 return mFile.getMimetype();
128 }
129
130 public boolean isInstant() {
131 return mIsInstant;
132 }
133
134 public boolean isRemoteFolderToBeCreated() {
135 return mRemoteFolderToBeCreated;
136 }
137
138 public void setRemoteFolderToBeCreated() {
139 mRemoteFolderToBeCreated = true;
140 }
141
142 public boolean getForceOverwrite() {
143 return mForceOverwrite;
144 }
145
146 public boolean wasRenamed() {
147 return mWasRenamed;
148 }
149
150 public Set<OnDatatransferProgressListener> getDataTransferListeners() {
151 return mDataTransferListeners;
152 }
153
154 public void addDatatransferProgressListener (OnDatatransferProgressListener listener) {
155 mDataTransferListeners.add(listener);
156 }
157
158 @Override
159 protected RemoteOperationResult run(WebdavClient client) {
160 RemoteOperationResult result = null;
161 boolean localCopyPassed = false, nameCheckPassed = false;
162 File temporalFile = null, originalFile = new File(mOriginalStoragePath), expectedFile = null;
163 try {
164 /// rename the file to upload, if necessary
165 if (!mForceOverwrite) {
166 String remotePath = getAvailableRemotePath(client, mRemotePath);
167 mWasRenamed = !remotePath.equals(mRemotePath);
168 if (mWasRenamed) {
169 createNewOCFile(remotePath);
170 }
171 }
172 nameCheckPassed = true;
173
174 String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, mFile); /// not before getAvailableRemotePath() !!!
175 expectedFile = new File(expectedPath);
176
177 /// check location of local file; if not the expected, copy to a temporal file before upload (if COPY is the expected behaviour)
178 if (!mOriginalStoragePath.equals(expectedPath) && mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_COPY) {
179
180 if (FileStorageUtils.getUsableSpace(mAccount.name) < originalFile.length()) {
181 result = new RemoteOperationResult(ResultCode.LOCAL_STORAGE_FULL);
182 return result; // error condition when the file should be copied
183
184 } else {
185 String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + mFile.getRemotePath();
186 mFile.setStoragePath(temporalPath);
187 temporalFile = new File(temporalPath);
188 if (!mOriginalStoragePath.equals(temporalPath)) { // preventing weird but possible situation
189 InputStream in = null;
190 OutputStream out = null;
191 try {
192 File temporalParent = temporalFile.getParentFile();
193 temporalParent.mkdirs();
194 if (!temporalParent.isDirectory()) {
195 throw new IOException("Unexpected error: parent directory could not be created");
196 }
197 temporalFile.createNewFile();
198 if (!temporalFile.isFile()) {
199 throw new IOException("Unexpected error: target file could not be created");
200 }
201 in = new FileInputStream(originalFile);
202 out = new FileOutputStream(temporalFile);
203 byte[] buf = new byte[1024];
204 int len;
205 while ((len = in.read(buf)) > 0){
206 out.write(buf, 0, len);
207 }
208
209 } catch (Exception e) {
210 result = new RemoteOperationResult(ResultCode.LOCAL_STORAGE_NOT_COPIED);
211 return result;
212
213 } finally {
214 try {
215 if (in != null) in.close();
216 } catch (Exception e) {
217 Log.d(TAG, "Weird exception while closing input stream for " + mOriginalStoragePath + " (ignoring)", e);
218 }
219 try {
220 if (out != null) out.close();
221 } catch (Exception e) {
222 Log.d(TAG, "Weird exception while closing output stream for " + expectedPath + " (ignoring)", e);
223 }
224 }
225 }
226 }
227 }
228 localCopyPassed = true;
229
230 /// perform the upload
231 synchronized(mCancellationRequested) {
232 if (mCancellationRequested.get()) {
233 throw new OperationCancelledException();
234 } else {
235 mPutMethod = new PutMethod(client.getBaseUri() + WebdavUtils.encodePath(mFile.getRemotePath()));
236 }
237 }
238 int status = uploadFile(client);
239
240
241 /// move local temporal file or original file to its corresponding location in the ownCloud local folder
242 if (isSuccess(status)) {
243 if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_FORGET) {
244 mFile.setStoragePath(null);
245
246 } else {
247 mFile.setStoragePath(expectedPath);
248 File fileToMove = null;
249 if (temporalFile != null) { // FileUploader.LOCAL_BEHAVIOUR_COPY ; see where temporalFile was set
250 fileToMove = temporalFile;
251 } else { // FileUploader.LOCAL_BEHAVIOUR_MOVE
252 fileToMove = originalFile;
253 }
254 if (!expectedFile.equals(fileToMove)) {
255 File expectedFolder = expectedFile.getParentFile();
256 expectedFolder.mkdirs();
257 if (!expectedFolder.isDirectory() || !fileToMove.renameTo(expectedFile)) {
258 mFile.setStoragePath(null); // forget the local file
259 // by now, treat this as a success; the file was uploaded; the user won't like that the local file is not linked, but this should be a veeery rare fail;
260 // the best option could be show a warning message (but not a fail)
261 //result = new RemoteOperationResult(ResultCode.LOCAL_STORAGE_NOT_MOVED);
262 //return result;
263 }
264 }
265 }
266 }
267
268 result = new RemoteOperationResult(isSuccess(status), status);
269
270
271 } catch (Exception e) {
272 // TODO something cleaner with cancellations
273 if (mCancellationRequested.get()) {
274 result = new RemoteOperationResult(new OperationCancelledException());
275 } else {
276 result = new RemoteOperationResult(e);
277 }
278
279
280 } finally {
281 if (temporalFile != null && !originalFile.equals(temporalFile)) {
282 temporalFile.delete();
283 }
284 if (result.isSuccess()) {
285 Log.i(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + ": " + result.getLogMessage());
286
287 } else {
288 if (result.getException() != null) {
289 String complement = "";
290 if (!nameCheckPassed) {
291 complement = " (while checking file existence in server)";
292 } else if (!localCopyPassed) {
293 complement = " (while copying local file to " + FileStorageUtils.getSavePath(mAccount.name) + ")";
294 }
295 Log.e(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + ": " + result.getLogMessage() + complement, result.getException());
296 } else {
297 Log.e(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + ": " + result.getLogMessage());
298 }
299 }
300 }
301
302 return result;
303 }
304
305
306 private void createNewOCFile(String newRemotePath) {
307 // a new OCFile instance must be created for a new remote path
308 OCFile newFile = new OCFile(newRemotePath);
309 newFile.setCreationTimestamp(mFile.getCreationTimestamp());
310 newFile.setFileLength(mFile.getFileLength());
311 newFile.setMimetype(mFile.getMimetype());
312 newFile.setModificationTimestamp(mFile.getModificationTimestamp());
313 // newFile.setEtag(mFile.getEtag())
314 newFile.setKeepInSync(mFile.keepInSync());
315 newFile.setLastSyncDateForProperties(mFile.getLastSyncDateForProperties());
316 newFile.setLastSyncDateForData(mFile.getLastSyncDateForData());
317 newFile.setStoragePath(mFile.getStoragePath());
318 newFile.setParentId(mFile.getParentId());
319 mOldFile = mFile;
320 mFile = newFile;
321 }
322
323
324 public boolean isSuccess(int status) {
325 return ((status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED || status == HttpStatus.SC_NO_CONTENT));
326 }
327
328
329 protected int uploadFile(WebdavClient client) throws HttpException, IOException, OperationCancelledException {
330 int status = -1;
331 try {
332 File f = new File(mFile.getStoragePath());
333 FileRequestEntity entity = new FileRequestEntity(f, getMimeType());
334 entity.addOnDatatransferProgressListeners(mDataTransferListeners);
335 mPutMethod.setRequestEntity(entity);
336 status = client.executeMethod(mPutMethod);
337 client.exhaustResponse(mPutMethod.getResponseBodyAsStream());
338
339 } finally {
340 mPutMethod.releaseConnection(); // let the connection available for other methods
341 }
342 return status;
343 }
344
345 /**
346 * Checks if remotePath does not exist in the server and returns it, or adds a suffix to it in order to avoid the server
347 * file is overwritten.
348 *
349 * @param string
350 * @return
351 */
352 private String getAvailableRemotePath(WebdavClient wc, String remotePath) throws Exception {
353 boolean check = wc.existsFile(remotePath);
354 if (!check) {
355 return remotePath;
356 }
357
358 int pos = remotePath.lastIndexOf(".");
359 String suffix = "";
360 String extension = "";
361 if (pos >= 0) {
362 extension = remotePath.substring(pos+1);
363 remotePath = remotePath.substring(0, pos);
364 }
365 int count = 2;
366 do {
367 suffix = " (" + count + ")";
368 if (pos >= 0)
369 check = wc.existsFile(remotePath + suffix + "." + extension);
370 else
371 check = wc.existsFile(remotePath + suffix);
372 count++;
373 } while (check);
374
375 if (pos >=0) {
376 return remotePath + suffix + "." + extension;
377 } else {
378 return remotePath + suffix;
379 }
380 }
381
382
383 public void cancel() {
384 synchronized(mCancellationRequested) {
385 mCancellationRequested.set(true);
386 if (mPutMethod != null)
387 mPutMethod.abort();
388 }
389 }
390
391
392 }