1 /* ownCloud Android client application 
   2  *   Copyright (C) 2012-2013 ownCloud Inc. 
   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. 
   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. 
  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/>. 
  18 package com
.owncloud
.android
.operations
; 
  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
.HashMap
; 
  27 import java
.util
.List
; 
  29 import java
.util
.Vector
; 
  31 import org
.apache
.http
.HttpStatus
; 
  32 import org
.apache
.jackrabbit
.webdav
.DavConstants
; 
  33 import org
.apache
.jackrabbit
.webdav
.MultiStatus
; 
  34 import org
.apache
.jackrabbit
.webdav
.client
.methods
.PropFindMethod
; 
  36 import android
.accounts
.Account
; 
  37 import android
.content
.Context
; 
  38 import android
.content
.Intent
; 
  40 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  41 import com
.owncloud
.android
.datamodel
.OCFile
; 
  42 import com
.owncloud
.android
.oc_framework
.network
.webdav
.WebdavClient
; 
  43 import com
.owncloud
.android
.oc_framework
.network
.webdav
.WebdavEntry
; 
  44 import com
.owncloud
.android
.oc_framework
.network
.webdav
.WebdavUtils
; 
  45 import com
.owncloud
.android
.oc_framework
.operations
.RemoteOperation
; 
  46 import com
.owncloud
.android
.oc_framework
.operations
.RemoteOperationResult
; 
  47 import com
.owncloud
.android
.oc_framework
.operations
.RemoteOperationResult
.ResultCode
; 
  48 import com
.owncloud
.android
.oc_framework
.operations
.remote
.ReadRemoteFileOperation
; 
  49 import com
.owncloud
.android
.syncadapter
.FileSyncService
; 
  50 import com
.owncloud
.android
.utils
.FileStorageUtils
; 
  51 import com
.owncloud
.android
.utils
.Log_OC
; 
  56  *  Remote operation performing the synchronization of the list of files contained  
  57  *  in a folder identified with its remote path. 
  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. 
  62  *  Does NOT enter in the child folders to synchronize their contents also. 
  64  *  @author David A. Velasco 
  66 public class SynchronizeFolderOperation 
extends RemoteOperation 
{ 
  68     private static final String TAG 
= SynchronizeFolderOperation
.class.getSimpleName(); 
  71     /** Time stamp for the synchronization process in progress */ 
  72     private long mCurrentSyncTime
; 
  74     /** Remote folder to synchronize */ 
  75     private OCFile mLocalFolder
; 
  77     /** Access to the local database */ 
  78     private FileDataStorageManager mStorageManager
; 
  80     /** Account where the file to synchronize belongs */ 
  81     private Account mAccount
; 
  83     /** Android context; necessary to send requests to the download service */ 
  84     private Context mContext
; 
  86     /** Files and folders contained in the synchronized folder after a successful operation */ 
  87     private List
<OCFile
> mChildren
; 
  89     /** Counter of conflicts found between local and remote files */ 
  90     private int mConflictsFound
; 
  92     /** Counter of failed operations in synchronization of kept-in-sync files */ 
  93     private int mFailsInFavouritesFound
; 
  95     /** Map of remote and local paths to files that where locally stored in a location out of the ownCloud folder and couldn't be copied automatically into it */ 
  96     private Map
<String
, String
> mForgottenLocalFiles
; 
  98     /** 'True' means that this operation is part of a full account synchronization */  
  99     private boolean mSyncFullAccount
; 
 101     /** 'True' means that the remote folder changed from last synchronization and should be fetched */ 
 102     private boolean mRemoteFolderChanged
; 
 106      * Creates a new instance of {@link SynchronizeFolderOperation}. 
 108      * @param   remoteFolderPath        Remote folder to synchronize. 
 109      * @param   currentSyncTime         Time stamp for the synchronization process in progress. 
 110      * @param   localFolderId           Identifier in the local database of the folder to synchronize. 
 111      * @param   updateFolderProperties  'True' means that the properties of the folder should be updated also, not just its content. 
 112      * @param   syncFullAccount         'True' means that this operation is part of a full account synchronization. 
 113      * @param   dataStorageManager      Interface with the local database. 
 114      * @param   account                 ownCloud account where the folder is located.  
 115      * @param   context                 Application context. 
 117     public SynchronizeFolderOperation(  OCFile folder
,  
 118                                         long currentSyncTime
,  
 119                                         boolean syncFullAccount
, 
 120                                         FileDataStorageManager dataStorageManager
,  
 123         mLocalFolder 
= folder
; 
 124         mCurrentSyncTime 
= currentSyncTime
; 
 125         mSyncFullAccount 
= syncFullAccount
; 
 126         mStorageManager 
= dataStorageManager
; 
 129         mForgottenLocalFiles 
= new HashMap
<String
, String
>(); 
 130         mRemoteFolderChanged 
= false
; 
 134     public int getConflictsFound() { 
 135         return mConflictsFound
; 
 138     public int getFailsInFavouritesFound() { 
 139         return mFailsInFavouritesFound
; 
 142     public Map
<String
, String
> getForgottenLocalFiles() { 
 143         return mForgottenLocalFiles
; 
 147      * Returns the list of files and folders contained in the synchronized folder, if called after synchronization is complete. 
 149      * @return  List of files and folders contained in the synchronized folder. 
 151     public List
<OCFile
> getChildren() { 
 156      * Performs the synchronization. 
 161     protected RemoteOperationResult 
run(WebdavClient client
) { 
 162         RemoteOperationResult result 
= null
; 
 163         mFailsInFavouritesFound 
= 0; 
 165         mForgottenLocalFiles
.clear(); 
 167         result 
= checkForChanges(client
); 
 169         if (result
.isSuccess()) { 
 170             if (mRemoteFolderChanged
) { 
 171                 result 
= fetchAndSyncRemoteFolder(client
); 
 173                 mChildren 
= mStorageManager
.getFolderContent(mLocalFolder
); 
 177         if (!mSyncFullAccount
) {             
 178             sendStickyBroadcast(false
, mLocalFolder
.getRemotePath(), result
); 
 186     private RemoteOperationResult 
checkForChanges(WebdavClient client
) { 
 187         mRemoteFolderChanged 
= false
; 
 188         RemoteOperationResult result 
= null
; 
 189         String remotePath 
= null
; 
 191             remotePath 
= mLocalFolder
.getRemotePath(); 
 192             Log_OC
.d(TAG
, "Checking changes in " + mAccount
.name 
+ remotePath
); 
 195             ReadRemoteFileOperation operation 
= new ReadRemoteFileOperation(remotePath
); 
 196             result 
= operation
.execute(client
); 
 197             if (result
.isSuccess()){ 
 198                 OCFile remoteFolder 
= FileStorageUtils
.fillOCFile(result
.getData().get(0)); 
 200              // check if remote and local folder are different 
 201               mRemoteFolderChanged 
= !(remoteFolder
.getEtag().equalsIgnoreCase(mLocalFolder
.getEtag())); 
 203               result 
= new RemoteOperationResult(ResultCode
.OK
); 
 205               Log_OC
.i(TAG
, "Checked " + mAccount
.name 
+ remotePath 
+ " : " + (mRemoteFolderChanged ? 
"changed" : "not changed")); 
 208                 if (result
.getCode() == ResultCode
.FILE_NOT_FOUND
) { 
 211                 if (result
.isException()) { 
 212                     Log_OC
.e(TAG
, "Checked " + mAccount
.name 
+ remotePath  
+ " : " + result
.getLogMessage(), result
.getException()); 
 214                     Log_OC
.e(TAG
, "Checked " + mAccount
.name 
+ remotePath 
+ " : " + result
.getLogMessage()); 
 222     private RemoteOperationResult 
fetchAndSyncRemoteFolder(WebdavClient client
) { 
 223         RemoteOperationResult result 
= null
; 
 224         String remotePath 
= null
; 
 225         PropFindMethod query 
= null
; 
 227             remotePath 
= mLocalFolder
.getRemotePath(); 
 228             Log_OC
.d(TAG
, "Synchronizing " + mAccount
.name 
+ remotePath
); 
 231             query 
= new PropFindMethod(client
.getBaseUri() + WebdavUtils
.encodePath(remotePath
), 
 232                     DavConstants
.PROPFIND_ALL_PROP
, 
 233                     DavConstants
.DEPTH_1
); 
 234             int status 
= client
.executeMethod(query
); 
 236             // check and process response 
 237             if (isMultiStatus(status
)) { 
 238                 synchronizeData(query
.getResponseBodyAsMultiStatus(), client
); 
 239                 if (mConflictsFound 
> 0  || mFailsInFavouritesFound 
> 0) {  
 240                     result 
= new RemoteOperationResult(ResultCode
.SYNC_CONFLICT
);   // should be different result, but will do the job 
 242                     result 
= new RemoteOperationResult(true
, status
, query
.getResponseHeaders()); 
 246                 // synchronization failed 
 247                 client
.exhaustResponse(query
.getResponseBodyAsStream()); 
 248                 if (status 
== HttpStatus
.SC_NOT_FOUND
) { 
 251                 result 
= new RemoteOperationResult(false
, status
, query
.getResponseHeaders()); 
 254         } catch (Exception e
) { 
 255             result 
= new RemoteOperationResult(e
); 
 260                 query
.releaseConnection();  // let the connection available for other methods 
 261             if (result
.isSuccess()) { 
 262                 Log_OC
.i(TAG
, "Synchronized " + mAccount
.name 
+ remotePath 
+ ": " + result
.getLogMessage()); 
 264                 if (result
.isException()) { 
 265                     Log_OC
.e(TAG
, "Synchronized " + mAccount
.name 
+ remotePath  
+ ": " + result
.getLogMessage(), result
.getException()); 
 267                     Log_OC
.e(TAG
, "Synchronized " + mAccount
.name 
+ remotePath 
+ ": " + result
.getLogMessage()); 
 276     private void removeLocalFolder() { 
 277         if (mStorageManager
.fileExists(mLocalFolder
.getFileId())) { 
 278             String currentSavePath 
= FileStorageUtils
.getSavePath(mAccount
.name
); 
 279             mStorageManager
.removeFolder(mLocalFolder
, true
, (mLocalFolder
.isDown() && mLocalFolder
.getStoragePath().startsWith(currentSavePath
))); 
 285      *  Synchronizes the data retrieved from the server about the contents of the target folder  
 286      *  with the current data in the local database. 
 288      *  Grants that mChildren is updated with fresh data after execution. 
 290      *  @param dataInServer     Full response got from the server with the data of the target  
 291      *                          folder and its direct children. 
 292      *  @param client           Client instance to the remote server where the data were  
 294      *  @return                 'True' when any change was made in the local data, 'false' otherwise. 
 296     private void synchronizeData(MultiStatus dataInServer
, WebdavClient client
) { 
 297         // get 'fresh data' from the database 
 298         mLocalFolder 
= mStorageManager
.getFileByPath(mLocalFolder
.getRemotePath()); 
 300         // parse data from remote folder  
 301         WebdavEntry we 
= new WebdavEntry(dataInServer
.getResponses()[0], client
.getBaseUri().getPath()); 
 302         OCFile remoteFolder 
= fillOCFile(we
); 
 303         remoteFolder
.setParentId(mLocalFolder
.getParentId()); 
 304         remoteFolder
.setFileId(mLocalFolder
.getFileId()); 
 306         Log_OC
.d(TAG
, "Remote folder " + mLocalFolder
.getRemotePath() + " changed - starting update of local data "); 
 308         List
<OCFile
> updatedFiles 
= new Vector
<OCFile
>(dataInServer
.getResponses().length 
- 1); 
 309         List
<SynchronizeFileOperation
> filesToSyncContents 
= new Vector
<SynchronizeFileOperation
>(); 
 311         // get current data about local contents of the folder to synchronize 
 312         List
<OCFile
> localFiles 
= mStorageManager
.getFolderContent(mLocalFolder
); 
 313         Map
<String
, OCFile
> localFilesMap 
= new HashMap
<String
, OCFile
>(localFiles
.size()); 
 314         for (OCFile file 
: localFiles
) { 
 315             localFilesMap
.put(file
.getRemotePath(), file
); 
 318         // loop to update every child 
 319         OCFile remoteFile 
= null
, localFile 
= null
; 
 320         for (int i 
= 1; i 
< dataInServer
.getResponses().length
; ++i
) { 
 321             /// new OCFile instance with the data from the server 
 322             we 
= new WebdavEntry(dataInServer
.getResponses()[i
], client
.getBaseUri().getPath());                         
 323             remoteFile 
= fillOCFile(we
); 
 324             remoteFile
.setParentId(mLocalFolder
.getFileId()); 
 326             /// retrieve local data for the read file  
 327             //localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath()); 
 328             localFile 
= localFilesMap
.remove(remoteFile
.getRemotePath()); 
 330             /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in the server side) 
 331             remoteFile
.setLastSyncDateForProperties(mCurrentSyncTime
); 
 332             if (localFile 
!= null
) { 
 333                 // some properties of local state are kept unmodified 
 334                 remoteFile
.setFileId(localFile
.getFileId()); 
 335                 remoteFile
.setKeepInSync(localFile
.keepInSync()); 
 336                 remoteFile
.setLastSyncDateForData(localFile
.getLastSyncDateForData()); 
 337                 remoteFile
.setModificationTimestampAtLastSyncForData(localFile
.getModificationTimestampAtLastSyncForData()); 
 338                 remoteFile
.setStoragePath(localFile
.getStoragePath()); 
 339                 remoteFile
.setEtag(localFile
.getEtag());    // eTag will not be updated unless contents are synchronized (Synchronize[File|Folder]Operation with remoteFile as parameter) 
 340                 if (remoteFile
.isFolder()) { 
 341                     remoteFile
.setFileLength(localFile
.getFileLength()); // TODO move operations about size of folders to FileContentProvider 
 344                 remoteFile
.setEtag(""); // remote eTag will not be updated unless contents are synchronized (Synchronize[File|Folder]Operation with remoteFile as parameter) 
 347             /// check and fix, if needed, local storage path 
 348             checkAndFixForeignStoragePath(remoteFile
);      // fixing old policy - now local files must be copied into the ownCloud local folder  
 349             searchForLocalFileInDefaultPath(remoteFile
);    // legacy    
 351             /// prepare content synchronization for kept-in-sync files 
 352             if (remoteFile
.keepInSync()) { 
 353                 SynchronizeFileOperation operation 
= new SynchronizeFileOperation(  localFile
,         
 360                 filesToSyncContents
.add(operation
); 
 363             updatedFiles
.add(remoteFile
); 
 366         // save updated contents in local database; all at once, trying to get a best performance in database update (not a big deal, indeed) 
 367         mStorageManager
.saveFolder(remoteFolder
, updatedFiles
, localFilesMap
.values()); 
 369         // request for the synchronization of file contents AFTER saving current remote properties 
 370         startContentSynchronizations(filesToSyncContents
, client
); 
 372         // removal of obsolete files 
 373         //removeObsoleteFiles(); 
 375         // must be done AFTER saving all the children information, so that eTag is not updated in the database in case of unexpected exceptions 
 376         //mStorageManager.saveFile(remoteFolder); 
 377         mChildren 
= updatedFiles
; 
 382      * Performs a list of synchronization operations, determining if a download or upload is needed or 
 383      * if exists conflict due to changes both in local and remote contents of the each file. 
 385      * If download or upload is needed, request the operation to the corresponding service and goes on. 
 387      * @param filesToSyncContents       Synchronization operations to execute. 
 388      * @param client                    Interface to the remote ownCloud server. 
 390     private void startContentSynchronizations(List
<SynchronizeFileOperation
> filesToSyncContents
, WebdavClient client
) { 
 391         RemoteOperationResult contentsResult 
= null
; 
 392         for (SynchronizeFileOperation op
: filesToSyncContents
) { 
 393             contentsResult 
= op
.execute(client
);   // returns without waiting for upload or download finishes 
 394             if (!contentsResult
.isSuccess()) { 
 395                 if (contentsResult
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 398                     mFailsInFavouritesFound
++; 
 399                     if (contentsResult
.getException() != null
) { 
 400                         Log_OC
.e(TAG
, "Error while synchronizing favourites : " +  contentsResult
.getLogMessage(), contentsResult
.getException()); 
 402                         Log_OC
.e(TAG
, "Error while synchronizing favourites : " + contentsResult
.getLogMessage()); 
 405             }   // won't let these fails break the synchronization process 
 410     public boolean isMultiStatus(int status
) { 
 411         return (status 
== HttpStatus
.SC_MULTI_STATUS
);  
 416      * Creates and populates a new {@link OCFile} object with the data read from the server. 
 418      * @param we        WebDAV entry read from the server for a WebDAV resource (remote file or folder). 
 419      * @return          New OCFile instance representing the remote resource described by we. 
 421     private OCFile 
fillOCFile(WebdavEntry we
) { 
 422         OCFile file 
= new OCFile(we
.decodedPath()); 
 423         file
.setCreationTimestamp(we
.createTimestamp()); 
 424         file
.setFileLength(we
.contentLength()); 
 425         file
.setMimetype(we
.contentType()); 
 426         file
.setModificationTimestamp(we
.modifiedTimestamp()); 
 427         file
.setEtag(we
.etag()); 
 433      * Checks the storage path of the OCFile received as parameter. If it's out of the local ownCloud folder, 
 434      * tries to copy the file inside it.  
 436      * If the copy fails, the link to the local file is nullified. The account of forgotten files is kept in  
 437      * {@link #mForgottenLocalFiles} 
 439      * @param file      File to check and fix. 
 441     private void checkAndFixForeignStoragePath(OCFile file
) { 
 442         String storagePath 
= file
.getStoragePath(); 
 443         String expectedPath 
= FileStorageUtils
.getDefaultSavePathFor(mAccount
.name
, file
); 
 444         if (storagePath 
!= null 
&& !storagePath
.equals(expectedPath
)) { 
 445             /// fix storagePaths out of the local ownCloud folder 
 446             File originalFile 
= new File(storagePath
); 
 447             if (FileStorageUtils
.getUsableSpace(mAccount
.name
) < originalFile
.length()) { 
 448                 mForgottenLocalFiles
.put(file
.getRemotePath(), storagePath
); 
 449                 file
.setStoragePath(null
); 
 452                 InputStream 
in = null
; 
 453                 OutputStream out 
= null
; 
 455                     File expectedFile 
= new File(expectedPath
); 
 456                     File expectedParent 
= expectedFile
.getParentFile(); 
 457                     expectedParent
.mkdirs(); 
 458                     if (!expectedParent
.isDirectory()) { 
 459                         throw new IOException("Unexpected error: parent directory could not be created"); 
 461                     expectedFile
.createNewFile(); 
 462                     if (!expectedFile
.isFile()) { 
 463                         throw new IOException("Unexpected error: target file could not be created"); 
 465                     in = new FileInputStream(originalFile
); 
 466                     out 
= new FileOutputStream(expectedFile
); 
 467                     byte[] buf 
= new byte[1024]; 
 469                     while ((len 
= in.read(buf
)) > 0){ 
 470                         out
.write(buf
, 0, len
); 
 472                     file
.setStoragePath(expectedPath
); 
 474                 } catch (Exception e
) { 
 475                     Log_OC
.e(TAG
, "Exception while copying foreign file " + expectedPath
, e
); 
 476                     mForgottenLocalFiles
.put(file
.getRemotePath(), storagePath
); 
 477                     file
.setStoragePath(null
); 
 481                         if (in != null
) in.close(); 
 482                     } catch (Exception e
) { 
 483                         Log_OC
.d(TAG
, "Weird exception while closing input stream for " + storagePath 
+ " (ignoring)", e
); 
 486                         if (out 
!= null
) out
.close(); 
 487                     } catch (Exception e
) { 
 488                         Log_OC
.d(TAG
, "Weird exception while closing output stream for " + expectedPath 
+ " (ignoring)", e
); 
 496      * Scans the default location for saving local copies of files searching for 
 497      * a 'lost' file with the same full name as the {@link OCFile} received as  
 500      * @param file      File to associate a possible 'lost' local file. 
 502     private void searchForLocalFileInDefaultPath(OCFile file
) { 
 503         if (file
.getStoragePath() == null 
&& !file
.isFolder()) { 
 504             File f 
= new File(FileStorageUtils
.getDefaultSavePathFor(mAccount
.name
, file
)); 
 506                 file
.setStoragePath(f
.getAbsolutePath()); 
 507                 file
.setLastSyncDateForData(f
.lastModified()); 
 514      * Sends a message to any application component interested in the progress of the synchronization. 
 516      * @param inProgress        'True' when the synchronization progress is not finished. 
 517      * @param dirRemotePath     Remote path of a folder that was just synchronized (with or without success) 
 519     private void sendStickyBroadcast(boolean inProgress
, String dirRemotePath
, RemoteOperationResult result
) { 
 520         Intent i 
= new Intent(FileSyncService
.getSyncMessage()); 
 521         i
.putExtra(FileSyncService
.IN_PROGRESS
, inProgress
); 
 522         i
.putExtra(FileSyncService
.ACCOUNT_NAME
, mAccount
.name
); 
 523         if (dirRemotePath 
!= null
) { 
 524             i
.putExtra(FileSyncService
.SYNC_FOLDER_REMOTE_PATH
, dirRemotePath
); 
 526         if (result 
!= null
) { 
 527             i
.putExtra(FileSyncService
.SYNC_RESULT
, result
); 
 529         mContext
.sendStickyBroadcast(i
); 
 533     public boolean getRemoteFolderChanged() { 
 534         return mRemoteFolderChanged
;