OC-1230: app doesn't show the folders structure correct for a while. Improve Sync...
[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.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Vector;
31
32 import org.apache.http.HttpStatus;
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 com.owncloud.android.Log_OC;
39 import com.owncloud.android.datamodel.DataStorageManager;
40 import com.owncloud.android.datamodel.OCFile;
41 import com.owncloud.android.operations.RemoteOperationResult.ResultCode;
42 import com.owncloud.android.utils.FileStorageUtils;
43
44 import eu.alefzero.webdav.WebdavClient;
45 import eu.alefzero.webdav.WebdavEntry;
46 import eu.alefzero.webdav.WebdavUtils;
47
48
49 /**
50 * Remote operation performing the synchronization a the contents of a remote folder with the local database
51 *
52 * @author David A. Velasco
53 */
54 public class SynchronizeFolderOperation extends RemoteOperation {
55
56 private static final String TAG = SynchronizeFolderOperation.class.getSimpleName();
57
58 /** Remote folder to synchronize */
59 private String mRemotePath;
60
61 /** Timestamp for the synchronization in progress */
62 private long mCurrentSyncTime;
63
64 /** Id of the folder to synchronize in the local database */
65 private long mParentId;
66
67 /** Boolean to indicate if is mandatory to update the folder */
68 private boolean mEnforceMetadataUpdate;
69
70 /** Access to the local database */
71 private DataStorageManager mStorageManager;
72
73 /** Account where the file to synchronize belongs */
74 private Account mAccount;
75
76 /** Android context; necessary to send requests to the download service; maybe something to refactor */
77 private Context mContext;
78
79 /** Files and folders contained in the synchronized folder */
80 private List<OCFile> mChildren;
81
82 private int mConflictsFound;
83
84 private int mFailsInFavouritesFound;
85
86 private Map<String, String> mForgottenLocalFiles;
87
88
89 public SynchronizeFolderOperation( String remotePath,
90 long currentSyncTime,
91 long parentId,
92 boolean enforceMetadataUpdate,
93 DataStorageManager dataStorageManager,
94 Account account,
95 Context context ) {
96 mRemotePath = remotePath;
97 mCurrentSyncTime = currentSyncTime;
98 mParentId = parentId;
99 mEnforceMetadataUpdate = enforceMetadataUpdate;
100 mStorageManager = dataStorageManager;
101 mAccount = account;
102 mContext = context;
103 mForgottenLocalFiles = new HashMap<String, String>();
104 }
105
106
107 public int getConflictsFound() {
108 return mConflictsFound;
109 }
110
111 public int getFailsInFavouritesFound() {
112 return mFailsInFavouritesFound;
113 }
114
115 public Map<String, String> getForgottenLocalFiles() {
116 return mForgottenLocalFiles;
117 }
118
119 /**
120 * Returns the list of files and folders contained in the synchronized folder, if called after synchronization is complete.
121 *
122 * @return List of files and folders contained in the synchronized folder.
123 */
124 public List<OCFile> getChildren() {
125 return mChildren;
126 }
127
128 public String getRemotePath() {
129 return mRemotePath;
130 }
131
132 public long getParentId() {
133 return mParentId;
134 }
135
136 @Override
137 protected RemoteOperationResult run(WebdavClient client) {
138 RemoteOperationResult result = null;
139 mFailsInFavouritesFound = 0;
140 mConflictsFound = 0;
141 mForgottenLocalFiles.clear();
142 boolean fileChanged = false;
143 boolean dirChanged = false;
144
145 // code before in FileSyncAdapter.fetchData
146 PropFindMethod query = null;
147 try {
148 Log_OC.d(TAG, "Synchronizing " + mAccount.name + ", fetching files in " + mRemotePath);
149
150 // remote request
151 query = new PropFindMethod(client.getBaseUri() + WebdavUtils.encodePath(mRemotePath));
152 int status = client.executeMethod(query);
153
154 // check and process response - /// TODO take into account all the possible status per child-resource
155 if (isMultiStatus(status)) {
156 MultiStatus resp = query.getResponseBodyAsMultiStatus();
157
158 // synchronize properties of the parent folder, if necessary
159 WebdavEntry we = new WebdavEntry(resp.getResponses()[0], client.getBaseUri().getPath());
160
161 // Properties of server folder
162 OCFile parent = fillOCFile(we);
163 // Properties of local folder
164 OCFile localParent = mStorageManager.getFileByPath(mRemotePath);
165 if (localParent == null || !(parent.getEtag().equalsIgnoreCase(localParent.getEtag())) || mEnforceMetadataUpdate) {
166 if (localParent != null) {
167 parent.setParentId(localParent.getParentId());
168 }
169 mStorageManager.saveFile(parent);
170 if (mParentId == DataStorageManager.ROOT_PARENT_ID)
171 mParentId = parent.getFileId();
172 dirChanged = true;
173 }
174
175 if (dirChanged) {
176 // read contents in folder
177 List<String> filesOnServer = new ArrayList<String> (); // Contains the lists of files on server
178 List<OCFile> updatedFiles = new Vector<OCFile>(resp.getResponses().length - 1);
179 List<SynchronizeFileOperation> filesToSyncContents = new Vector<SynchronizeFileOperation>();
180 for (int i = 1; i < resp.getResponses().length; ++i) {
181 /// new OCFile instance with the data from the server
182 we = new WebdavEntry(resp.getResponses()[i], client.getBaseUri().getPath());
183 OCFile file = fillOCFile(we);
184
185 filesOnServer.add(file.getRemotePath()); // Registry the file in the list
186
187 /// set data about local state, keeping unchanged former data if existing
188 file.setLastSyncDateForProperties(mCurrentSyncTime);
189 OCFile oldFile = mStorageManager.getFileByPath(file.getRemotePath());
190
191 // Check if it is needed to synchronize the folder
192 fileChanged = false;
193 if (oldFile != null) {
194 if (!file.getEtag().equalsIgnoreCase(oldFile.getEtag())) {
195 fileChanged = true;
196 }
197 } else
198 fileChanged= true;
199
200
201 if (fileChanged){
202 if (oldFile != null) {
203 file.setKeepInSync(oldFile.keepInSync());
204 file.setLastSyncDateForData(oldFile.getLastSyncDateForData());
205 file.setModificationTimestampAtLastSyncForData(oldFile.getModificationTimestampAtLastSyncForData()); // must be kept unchanged when the file contents are not updated
206 checkAndFixForeignStoragePath(oldFile);
207 file.setStoragePath(oldFile.getStoragePath());
208 if (file.isDirectory())
209 file.setEtag(oldFile.getEtag());
210 } else
211 if (file.isDirectory())
212 file.setEtag("");
213
214 /// scan default location if local copy of file is not linked in OCFile instance
215 if (file.getStoragePath() == null && !file.isDirectory()) {
216 File f = new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, file));
217 if (f.exists()) {
218 file.setStoragePath(f.getAbsolutePath());
219 file.setLastSyncDateForData(f.lastModified());
220 }
221 }
222
223 /// prepare content synchronization for kept-in-sync files
224 if (file.keepInSync()) {
225 SynchronizeFileOperation operation = new SynchronizeFileOperation( oldFile,
226 file,
227 mStorageManager,
228 mAccount,
229 true,
230 false,
231 mContext
232 );
233 filesToSyncContents.add(operation);
234 }
235
236 updatedFiles.add(file);
237 }
238 }
239
240 // save updated contents in local database; all at once, trying to get a best performance in database update (not a big deal, indeed)
241 mStorageManager.saveFiles(updatedFiles);
242
243 // request for the synchronization of files AFTER saving last properties
244 RemoteOperationResult contentsResult = null;
245 for (SynchronizeFileOperation op: filesToSyncContents) {
246 contentsResult = op.execute(client); // returns without waiting for upload or download finishes
247 if (!contentsResult.isSuccess()) {
248 if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) {
249 mConflictsFound++;
250 } else {
251 mFailsInFavouritesFound++;
252 if (contentsResult.getException() != null) {
253 Log_OC.e(TAG, "Error while synchronizing favourites : " + contentsResult.getLogMessage(), contentsResult.getException());
254 } else {
255 Log_OC.e(TAG, "Error while synchronizing favourites : " + contentsResult.getLogMessage());
256 }
257 }
258 } // won't let these fails break the synchronization process
259 }
260
261
262 // removal of obsolete files
263 mChildren = mStorageManager.getDirectoryContent(mStorageManager.getFileById(mParentId));
264 String currentSavePath = FileStorageUtils.getSavePath(mAccount.name);
265 for (OCFile fileChild: mChildren) {
266 if (!filesOnServer.contains(fileChild.getRemotePath())) {
267 Log_OC.d(TAG, "removing file: " + fileChild.getFileName());
268 mStorageManager.removeFile(fileChild, (fileChild.isDown() && fileChild.getStoragePath().startsWith(currentSavePath)));
269 // mChildren.remove(fileChild); //.remove(i);
270 }
271 }
272
273 } else {
274 client.exhaustResponse(query.getResponseBodyAsStream());
275 }
276 }
277
278 // prepare result object
279 if (!dirChanged) {
280 result = new RemoteOperationResult(ResultCode.OK_NO_CHANGES_ON_DIR);
281 mChildren = mStorageManager.getDirectoryContent(mStorageManager.getFileById(mParentId));
282
283 } else if (isMultiStatus(status)) {
284 if (mConflictsFound > 0 || mFailsInFavouritesFound > 0) {
285 result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); // should be different result, but will do the job
286
287 } else {
288 result = new RemoteOperationResult(true, status, query.getResponseHeaders());
289 }
290 } else {
291 result = new RemoteOperationResult(false, status, query.getResponseHeaders());
292 }
293 Log_OC.i(TAG, "Synchronizing " + mAccount.name + ", folder " + mRemotePath + ": " + result.getLogMessage());
294
295 } catch (Exception e) {
296 result = new RemoteOperationResult(e);
297
298
299 } finally {
300 if (query != null)
301 query.releaseConnection(); // let the connection available for other methods
302 if (result.isSuccess()) {
303 Log_OC.i(TAG, "Synchronizing " + mAccount.name + ", folder " + mRemotePath + ": " + result.getLogMessage());
304 } else {
305 if (result.isException()) {
306 Log_OC.e(TAG, "Synchronizing " + mAccount.name + ", folder " + mRemotePath + ": " + result.getLogMessage(), result.getException());
307 } else {
308 Log_OC.e(TAG, "Synchronizing " + mAccount.name + ", folder " + mRemotePath + ": " + result.getLogMessage());
309 }
310 }
311 }
312
313 return result;
314 }
315
316
317 public boolean isMultiStatus(int status) {
318 return (status == HttpStatus.SC_MULTI_STATUS);
319 }
320
321
322 /**
323 * Creates and populates a new {@link OCFile} object with the data read from the server.
324 *
325 * @param we WebDAV entry read from the server for a WebDAV resource (remote file or folder).
326 * @return New OCFile instance representing the remote resource described by we.
327 */
328 private OCFile fillOCFile(WebdavEntry we) {
329 OCFile file = new OCFile(we.decodedPath());
330 file.setCreationTimestamp(we.createTimestamp());
331 file.setFileLength(we.contentLength());
332 file.setMimetype(we.contentType());
333 file.setModificationTimestamp(we.modifiedTimestamp());
334 file.setParentId(mParentId);
335 file.setEtag(we.etag());
336 return file;
337 }
338
339
340 /**
341 * Checks the storage path of the OCFile received as parameter. If it's out of the local ownCloud folder,
342 * tries to copy the file inside it.
343 *
344 * If the copy fails, the link to the local file is nullified. The account of forgotten files is kept in
345 * {@link #mForgottenLocalFiles}
346 *)
347 * @param file File to check and fix.
348 */
349 private void checkAndFixForeignStoragePath(OCFile file) {
350 String storagePath = file.getStoragePath();
351 String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, file);
352 if (storagePath != null && !storagePath.equals(expectedPath)) {
353 /// fix storagePaths out of the local ownCloud folder
354 File originalFile = new File(storagePath);
355 if (FileStorageUtils.getUsableSpace(mAccount.name) < originalFile.length()) {
356 mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
357 file.setStoragePath(null);
358
359 } else {
360 InputStream in = null;
361 OutputStream out = null;
362 try {
363 File expectedFile = new File(expectedPath);
364 File expectedParent = expectedFile.getParentFile();
365 expectedParent.mkdirs();
366 if (!expectedParent.isDirectory()) {
367 throw new IOException("Unexpected error: parent directory could not be created");
368 }
369 expectedFile.createNewFile();
370 if (!expectedFile.isFile()) {
371 throw new IOException("Unexpected error: target file could not be created");
372 }
373 in = new FileInputStream(originalFile);
374 out = new FileOutputStream(expectedFile);
375 byte[] buf = new byte[1024];
376 int len;
377 while ((len = in.read(buf)) > 0){
378 out.write(buf, 0, len);
379 }
380 file.setStoragePath(expectedPath);
381
382 } catch (Exception e) {
383 Log_OC.e(TAG, "Exception while copying foreign file " + expectedPath, e);
384 mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
385 file.setStoragePath(null);
386
387 } finally {
388 try {
389 if (in != null) in.close();
390 } catch (Exception e) {
391 Log_OC.d(TAG, "Weird exception while closing input stream for " + storagePath + " (ignoring)", e);
392 }
393 try {
394 if (out != null) out.close();
395 } catch (Exception e) {
396 Log_OC.d(TAG, "Weird exception while closing output stream for " + expectedPath + " (ignoring)", e);
397 }
398 }
399 }
400 }
401 }
402
403
404 }