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