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