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