Fix, creation of subdirectories
[pub/Android/ownCloud.git] / src / com / owncloud / android / operations / SynchronizeFolderOperation.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012-2013 ownCloud Inc.
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 version 2,
6 * as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 */
17
18 package com.owncloud.android.operations;
19
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.OutputStream;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Vector;
30
31 import org.apache.http.HttpStatus;
32 import org.apache.jackrabbit.webdav.DavConstants;
33 import org.apache.jackrabbit.webdav.MultiStatus;
34 import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
35
36 import android.accounts.Account;
37 import android.content.Context;
38 import android.content.Intent;
39
40 import com.owncloud.android.datamodel.FileDataStorageManager;
41 import com.owncloud.android.datamodel.OCFile;
42 import com.owncloud.android.oc_framework.network.webdav.WebdavClient;
43 import com.owncloud.android.oc_framework.network.webdav.WebdavEntry;
44 import com.owncloud.android.oc_framework.network.webdav.WebdavUtils;
45 import com.owncloud.android.oc_framework.operations.RemoteOperation;
46 import com.owncloud.android.oc_framework.operations.RemoteOperationResult;
47 import com.owncloud.android.oc_framework.operations.RemoteOperationResult.ResultCode;
48 import com.owncloud.android.syncadapter.FileSyncService;
49 import com.owncloud.android.utils.FileStorageUtils;
50 import com.owncloud.android.utils.Log_OC;
51
52
53
54 /**
55 * Remote operation performing the synchronization of the list of files contained
56 * in a folder identified with its remote path.
57 *
58 * Fetches the list and properties of the files contained in the given folder, including their
59 * properties, and updates the local database with them.
60 *
61 * Does NOT enter in the child folders to synchronize their contents also.
62 *
63 * @author David A. Velasco
64 */
65 public class SynchronizeFolderOperation extends RemoteOperation {
66
67 private static final String TAG = SynchronizeFolderOperation.class.getSimpleName();
68
69
70 /** Time stamp for the synchronization process in progress */
71 private long mCurrentSyncTime;
72
73 /** Remote folder to synchronize */
74 private OCFile mLocalFolder;
75
76 /** Access to the local database */
77 private FileDataStorageManager mStorageManager;
78
79 /** Account where the file to synchronize belongs */
80 private Account mAccount;
81
82 /** Android context; necessary to send requests to the download service */
83 private Context mContext;
84
85 /** Files and folders contained in the synchronized folder after a successful operation */
86 private List<OCFile> mChildren;
87
88 /** Counter of conflicts found between local and remote files */
89 private int mConflictsFound;
90
91 /** Counter of failed operations in synchronization of kept-in-sync files */
92 private int mFailsInFavouritesFound;
93
94 /** Map of remote and local paths to files that where locally stored in a location out of the ownCloud folder and couldn't be copied automatically into it */
95 private Map<String, String> mForgottenLocalFiles;
96
97 /** 'True' means that this operation is part of a full account synchronization */
98 private boolean mSyncFullAccount;
99
100 /** 'True' means that the remote folder changed from last synchronization and should be fetched */
101 private boolean mRemoteFolderChanged;
102
103
104 /**
105 * Creates a new instance of {@link SynchronizeFolderOperation}.
106 *
107 * @param remoteFolderPath Remote folder to synchronize.
108 * @param currentSyncTime Time stamp for the synchronization process in progress.
109 * @param localFolderId Identifier in the local database of the folder to synchronize.
110 * @param updateFolderProperties 'True' means that the properties of the folder should be updated also, not just its content.
111 * @param syncFullAccount 'True' means that this operation is part of a full account synchronization.
112 * @param dataStorageManager Interface with the local database.
113 * @param account ownCloud account where the folder is located.
114 * @param context Application context.
115 */
116 public SynchronizeFolderOperation( OCFile folder,
117 long currentSyncTime,
118 boolean syncFullAccount,
119 FileDataStorageManager dataStorageManager,
120 Account account,
121 Context context ) {
122 mLocalFolder = folder;
123 mCurrentSyncTime = currentSyncTime;
124 mSyncFullAccount = syncFullAccount;
125 mStorageManager = dataStorageManager;
126 mAccount = account;
127 mContext = context;
128 mForgottenLocalFiles = new HashMap<String, String>();
129 mRemoteFolderChanged = false;
130 }
131
132
133 public int getConflictsFound() {
134 return mConflictsFound;
135 }
136
137 public int getFailsInFavouritesFound() {
138 return mFailsInFavouritesFound;
139 }
140
141 public Map<String, String> getForgottenLocalFiles() {
142 return mForgottenLocalFiles;
143 }
144
145 /**
146 * Returns the list of files and folders contained in the synchronized folder, if called after synchronization is complete.
147 *
148 * @return List of files and folders contained in the synchronized folder.
149 */
150 public List<OCFile> getChildren() {
151 return mChildren;
152 }
153
154 /**
155 * Performs the synchronization.
156 *
157 * {@inheritDoc}
158 */
159 @Override
160 protected RemoteOperationResult run(WebdavClient client) {
161 RemoteOperationResult result = null;
162 mFailsInFavouritesFound = 0;
163 mConflictsFound = 0;
164 mForgottenLocalFiles.clear();
165
166 result = checkForChanges(client);
167
168 if (result.isSuccess()) {
169 if (mRemoteFolderChanged) {
170 result = fetchAndSyncRemoteFolder(client);
171 } else {
172 mChildren = mStorageManager.getFolderContent(mLocalFolder);
173 }
174 }
175
176 if (!mSyncFullAccount) {
177 sendStickyBroadcast(false, mLocalFolder.getRemotePath(), result);
178 }
179
180 return result;
181
182 }
183
184
185 private RemoteOperationResult checkForChanges(WebdavClient client) {
186 mRemoteFolderChanged = false;
187 RemoteOperationResult result = null;
188 String remotePath = null;
189 PropFindMethod query = null;
190
191 try {
192 remotePath = mLocalFolder.getRemotePath();
193 Log_OC.d(TAG, "Checking changes in " + mAccount.name + remotePath);
194
195 // remote request
196 query = new PropFindMethod(client.getBaseUri() + WebdavUtils.encodePath(remotePath),
197 DavConstants.PROPFIND_ALL_PROP,
198 DavConstants.DEPTH_0);
199 int status = client.executeMethod(query);
200
201 // check and process response
202 if (isMultiStatus(status)) {
203 // parse data from remote folder
204 WebdavEntry we = new WebdavEntry(query.getResponseBodyAsMultiStatus().getResponses()[0], client.getBaseUri().getPath());
205 OCFile remoteFolder = fillOCFile(we);
206
207 // check if remote and local folder are different
208 mRemoteFolderChanged = !(remoteFolder.getEtag().equalsIgnoreCase(mLocalFolder.getEtag()));
209
210 result = new RemoteOperationResult(ResultCode.OK);
211
212 } else {
213 // check failed
214 client.exhaustResponse(query.getResponseBodyAsStream());
215 if (status == HttpStatus.SC_NOT_FOUND) {
216 removeLocalFolder();
217 }
218 result = new RemoteOperationResult(false, status, query.getResponseHeaders());
219 }
220
221 } catch (Exception e) {
222 result = new RemoteOperationResult(e);
223
224
225 } finally {
226 if (query != null)
227 query.releaseConnection(); // let the connection available for other methods
228 if (result.isSuccess()) {
229 Log_OC.i(TAG, "Checked " + mAccount.name + remotePath + " : " + (mRemoteFolderChanged ? "changed" : "not changed"));
230 } else {
231 if (result.isException()) {
232 Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + result.getLogMessage(), result.getException());
233 } else {
234 Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + result.getLogMessage());
235 }
236 }
237
238 }
239 return result;
240 }
241
242
243 private RemoteOperationResult fetchAndSyncRemoteFolder(WebdavClient client) {
244 RemoteOperationResult result = null;
245 String remotePath = null;
246 PropFindMethod query = null;
247 try {
248 remotePath = mLocalFolder.getRemotePath();
249 Log_OC.d(TAG, "Synchronizing " + mAccount.name + remotePath);
250
251 // remote request
252 query = new PropFindMethod(client.getBaseUri() + WebdavUtils.encodePath(remotePath),
253 DavConstants.PROPFIND_ALL_PROP,
254 DavConstants.DEPTH_1);
255 int status = client.executeMethod(query);
256
257 // check and process response
258 if (isMultiStatus(status)) {
259 synchronizeData(query.getResponseBodyAsMultiStatus(), client);
260 if (mConflictsFound > 0 || mFailsInFavouritesFound > 0) {
261 result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); // should be different result, but will do the job
262 } else {
263 result = new RemoteOperationResult(true, status, query.getResponseHeaders());
264 }
265
266 } else {
267 // synchronization failed
268 client.exhaustResponse(query.getResponseBodyAsStream());
269 if (status == HttpStatus.SC_NOT_FOUND) {
270 removeLocalFolder();
271 }
272 result = new RemoteOperationResult(false, status, query.getResponseHeaders());
273 }
274
275 } catch (Exception e) {
276 result = new RemoteOperationResult(e);
277
278
279 } finally {
280 if (query != null)
281 query.releaseConnection(); // let the connection available for other methods
282 if (result.isSuccess()) {
283 Log_OC.i(TAG, "Synchronized " + mAccount.name + remotePath + ": " + result.getLogMessage());
284 } else {
285 if (result.isException()) {
286 Log_OC.e(TAG, "Synchronized " + mAccount.name + remotePath + ": " + result.getLogMessage(), result.getException());
287 } else {
288 Log_OC.e(TAG, "Synchronized " + mAccount.name + remotePath + ": " + result.getLogMessage());
289 }
290 }
291
292 }
293 return result;
294 }
295
296
297 private void removeLocalFolder() {
298 if (mStorageManager.fileExists(mLocalFolder.getFileId())) {
299 String currentSavePath = FileStorageUtils.getSavePath(mAccount.name);
300 mStorageManager.removeFolder(mLocalFolder, true, (mLocalFolder.isDown() && mLocalFolder.getStoragePath().startsWith(currentSavePath)));
301 }
302 }
303
304
305 /**
306 * Synchronizes the data retrieved from the server about the contents of the target folder
307 * with the current data in the local database.
308 *
309 * Grants that mChildren is updated with fresh data after execution.
310 *
311 * @param dataInServer Full response got from the server with the data of the target
312 * folder and its direct children.
313 * @param client Client instance to the remote server where the data were
314 * retrieved.
315 * @return 'True' when any change was made in the local data, 'false' otherwise.
316 */
317 private void synchronizeData(MultiStatus dataInServer, WebdavClient client) {
318 // get 'fresh data' from the database
319 mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath());
320
321 // parse data from remote folder
322 WebdavEntry we = new WebdavEntry(dataInServer.getResponses()[0], client.getBaseUri().getPath());
323 OCFile remoteFolder = fillOCFile(we);
324 remoteFolder.setParentId(mLocalFolder.getParentId());
325 remoteFolder.setFileId(mLocalFolder.getFileId());
326
327 Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() + " changed - starting update of local data ");
328
329 List<OCFile> updatedFiles = new Vector<OCFile>(dataInServer.getResponses().length - 1);
330 List<SynchronizeFileOperation> filesToSyncContents = new Vector<SynchronizeFileOperation>();
331
332 // get current data about local contents of the folder to synchronize
333 List<OCFile> localFiles = mStorageManager.getFolderContent(mLocalFolder);
334 Map<String, OCFile> localFilesMap = new HashMap<String, OCFile>(localFiles.size());
335 for (OCFile file : localFiles) {
336 localFilesMap.put(file.getRemotePath(), file);
337 }
338
339 // loop to update every child
340 OCFile remoteFile = null, localFile = null;
341 for (int i = 1; i < dataInServer.getResponses().length; ++i) {
342 /// new OCFile instance with the data from the server
343 we = new WebdavEntry(dataInServer.getResponses()[i], client.getBaseUri().getPath());
344 remoteFile = fillOCFile(we);
345 remoteFile.setParentId(mLocalFolder.getFileId());
346
347 /// retrieve local data for the read file
348 //localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath());
349 localFile = localFilesMap.remove(remoteFile.getRemotePath());
350
351 /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in the server side)
352 remoteFile.setLastSyncDateForProperties(mCurrentSyncTime);
353 if (localFile != null) {
354 // some properties of local state are kept unmodified
355 remoteFile.setFileId(localFile.getFileId());
356 remoteFile.setKeepInSync(localFile.keepInSync());
357 remoteFile.setLastSyncDateForData(localFile.getLastSyncDateForData());
358 remoteFile.setModificationTimestampAtLastSyncForData(localFile.getModificationTimestampAtLastSyncForData());
359 remoteFile.setStoragePath(localFile.getStoragePath());
360 remoteFile.setEtag(localFile.getEtag()); // eTag will not be updated unless contents are synchronized (Synchronize[File|Folder]Operation with remoteFile as parameter)
361 if (remoteFile.isFolder()) {
362 remoteFile.setFileLength(localFile.getFileLength()); // TODO move operations about size of folders to FileContentProvider
363 }
364 } else {
365 remoteFile.setEtag(""); // remote eTag will not be updated unless contents are synchronized (Synchronize[File|Folder]Operation with remoteFile as parameter)
366 }
367
368 /// check and fix, if needed, local storage path
369 checkAndFixForeignStoragePath(remoteFile); // fixing old policy - now local files must be copied into the ownCloud local folder
370 searchForLocalFileInDefaultPath(remoteFile); // legacy
371
372 /// prepare content synchronization for kept-in-sync files
373 if (remoteFile.keepInSync()) {
374 SynchronizeFileOperation operation = new SynchronizeFileOperation( localFile,
375 remoteFile,
376 mStorageManager,
377 mAccount,
378 true,
379 mContext
380 );
381 filesToSyncContents.add(operation);
382 }
383
384 updatedFiles.add(remoteFile);
385 }
386
387 // save updated contents in local database; all at once, trying to get a best performance in database update (not a big deal, indeed)
388 mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
389
390 // request for the synchronization of file contents AFTER saving current remote properties
391 startContentSynchronizations(filesToSyncContents, client);
392
393 // removal of obsolete files
394 //removeObsoleteFiles();
395
396 // must be done AFTER saving all the children information, so that eTag is not updated in the database in case of unexpected exceptions
397 //mStorageManager.saveFile(remoteFolder);
398 mChildren = updatedFiles;
399
400 }
401
402 /**
403 * Performs a list of synchronization operations, determining if a download or upload is needed or
404 * if exists conflict due to changes both in local and remote contents of the each file.
405 *
406 * If download or upload is needed, request the operation to the corresponding service and goes on.
407 *
408 * @param filesToSyncContents Synchronization operations to execute.
409 * @param client Interface to the remote ownCloud server.
410 */
411 private void startContentSynchronizations(List<SynchronizeFileOperation> filesToSyncContents, WebdavClient client) {
412 RemoteOperationResult contentsResult = null;
413 for (SynchronizeFileOperation op: filesToSyncContents) {
414 contentsResult = op.execute(client); // returns without waiting for upload or download finishes
415 if (!contentsResult.isSuccess()) {
416 if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) {
417 mConflictsFound++;
418 } else {
419 mFailsInFavouritesFound++;
420 if (contentsResult.getException() != null) {
421 Log_OC.e(TAG, "Error while synchronizing favourites : " + contentsResult.getLogMessage(), contentsResult.getException());
422 } else {
423 Log_OC.e(TAG, "Error while synchronizing favourites : " + contentsResult.getLogMessage());
424 }
425 }
426 } // won't let these fails break the synchronization process
427 }
428 }
429
430
431 public boolean isMultiStatus(int status) {
432 return (status == HttpStatus.SC_MULTI_STATUS);
433 }
434
435
436 /**
437 * Creates and populates a new {@link OCFile} object with the data read from the server.
438 *
439 * @param we WebDAV entry read from the server for a WebDAV resource (remote file or folder).
440 * @return New OCFile instance representing the remote resource described by we.
441 */
442 private OCFile fillOCFile(WebdavEntry we) {
443 OCFile file = new OCFile(we.decodedPath());
444 file.setCreationTimestamp(we.createTimestamp());
445 file.setFileLength(we.contentLength());
446 file.setMimetype(we.contentType());
447 file.setModificationTimestamp(we.modifiedTimestamp());
448 file.setEtag(we.etag());
449 return file;
450 }
451
452
453 /**
454 * Checks the storage path of the OCFile received as parameter. If it's out of the local ownCloud folder,
455 * tries to copy the file inside it.
456 *
457 * If the copy fails, the link to the local file is nullified. The account of forgotten files is kept in
458 * {@link #mForgottenLocalFiles}
459 *)
460 * @param file File to check and fix.
461 */
462 private void checkAndFixForeignStoragePath(OCFile file) {
463 String storagePath = file.getStoragePath();
464 String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, file);
465 if (storagePath != null && !storagePath.equals(expectedPath)) {
466 /// fix storagePaths out of the local ownCloud folder
467 File originalFile = new File(storagePath);
468 if (FileStorageUtils.getUsableSpace(mAccount.name) < originalFile.length()) {
469 mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
470 file.setStoragePath(null);
471
472 } else {
473 InputStream in = null;
474 OutputStream out = null;
475 try {
476 File expectedFile = new File(expectedPath);
477 File expectedParent = expectedFile.getParentFile();
478 expectedParent.mkdirs();
479 if (!expectedParent.isDirectory()) {
480 throw new IOException("Unexpected error: parent directory could not be created");
481 }
482 expectedFile.createNewFile();
483 if (!expectedFile.isFile()) {
484 throw new IOException("Unexpected error: target file could not be created");
485 }
486 in = new FileInputStream(originalFile);
487 out = new FileOutputStream(expectedFile);
488 byte[] buf = new byte[1024];
489 int len;
490 while ((len = in.read(buf)) > 0){
491 out.write(buf, 0, len);
492 }
493 file.setStoragePath(expectedPath);
494
495 } catch (Exception e) {
496 Log_OC.e(TAG, "Exception while copying foreign file " + expectedPath, e);
497 mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
498 file.setStoragePath(null);
499
500 } finally {
501 try {
502 if (in != null) in.close();
503 } catch (Exception e) {
504 Log_OC.d(TAG, "Weird exception while closing input stream for " + storagePath + " (ignoring)", e);
505 }
506 try {
507 if (out != null) out.close();
508 } catch (Exception e) {
509 Log_OC.d(TAG, "Weird exception while closing output stream for " + expectedPath + " (ignoring)", e);
510 }
511 }
512 }
513 }
514 }
515
516 /**
517 * Scans the default location for saving local copies of files searching for
518 * a 'lost' file with the same full name as the {@link OCFile} received as
519 * parameter.
520 *
521 * @param file File to associate a possible 'lost' local file.
522 */
523 private void searchForLocalFileInDefaultPath(OCFile file) {
524 if (file.getStoragePath() == null && !file.isFolder()) {
525 File f = new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, file));
526 if (f.exists()) {
527 file.setStoragePath(f.getAbsolutePath());
528 file.setLastSyncDateForData(f.lastModified());
529 }
530 }
531 }
532
533
534 /**
535 * Sends a message to any application component interested in the progress of the synchronization.
536 *
537 * @param inProgress 'True' when the synchronization progress is not finished.
538 * @param dirRemotePath Remote path of a folder that was just synchronized (with or without success)
539 */
540 private void sendStickyBroadcast(boolean inProgress, String dirRemotePath, RemoteOperationResult result) {
541 Intent i = new Intent(FileSyncService.getSyncMessage());
542 i.putExtra(FileSyncService.IN_PROGRESS, inProgress);
543 i.putExtra(FileSyncService.ACCOUNT_NAME, mAccount.name);
544 if (dirRemotePath != null) {
545 i.putExtra(FileSyncService.SYNC_FOLDER_REMOTE_PATH, dirRemotePath);
546 }
547 if (result != null) {
548 i.putExtra(FileSyncService.SYNC_RESULT, result);
549 }
550 mContext.sendStickyBroadcast(i);
551 }
552
553
554 public boolean getRemoteFolderChanged() {
555 return mRemoteFolderChanged;
556 }
557
558 }