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