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