SynchronizeFolderOperation.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. /*
  2. * ownCloud Android client application
  3. *
  4. * @author David A. Velasco
  5. * Copyright (C) 2016 ownCloud Inc.
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License version 2,
  9. * as published by the Free Software Foundation.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. *
  19. */
  20. package com.owncloud.android.operations;
  21. import android.content.Context;
  22. import android.content.Intent;
  23. import android.text.TextUtils;
  24. import com.nextcloud.client.account.User;
  25. import com.nextcloud.client.files.downloader.FileDownloadHelper;
  26. import com.owncloud.android.datamodel.DecryptedFolderMetadata;
  27. import com.owncloud.android.datamodel.FileDataStorageManager;
  28. import com.owncloud.android.datamodel.OCFile;
  29. import com.owncloud.android.lib.common.OwnCloudClient;
  30. import com.owncloud.android.lib.common.operations.OperationCancelledException;
  31. import com.owncloud.android.lib.common.operations.RemoteOperationResult;
  32. import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
  33. import com.owncloud.android.lib.common.utils.Log_OC;
  34. import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
  35. import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation;
  36. import com.owncloud.android.lib.resources.files.model.RemoteFile;
  37. import com.owncloud.android.operations.common.SyncOperation;
  38. import com.owncloud.android.services.OperationsService;
  39. import com.owncloud.android.utils.FileStorageUtils;
  40. import com.owncloud.android.utils.MimeTypeUtil;
  41. import java.io.File;
  42. import java.util.ArrayList;
  43. import java.util.List;
  44. import java.util.Map;
  45. import java.util.Vector;
  46. import java.util.concurrent.atomic.AtomicBoolean;
  47. /**
  48. * Remote operation performing the synchronization of the list of files contained
  49. * in a folder identified with its remote path.
  50. *
  51. * Fetches the list and properties of the files contained in the given folder, including their
  52. * properties, and updates the local database with them.
  53. *
  54. * Does NOT enter in the child folders to synchronize their contents also, BUT requests for a new operation instance
  55. * doing so.
  56. */
  57. public class SynchronizeFolderOperation extends SyncOperation {
  58. private static final String TAG = SynchronizeFolderOperation.class.getSimpleName();
  59. /** Time stamp for the synchronization process in progress */
  60. private long mCurrentSyncTime;
  61. /** Remote path of the folder to synchronize */
  62. private String mRemotePath;
  63. /** Account where the file to synchronize belongs */
  64. private User user;
  65. /** Android context; necessary to send requests to the download service */
  66. private Context mContext;
  67. /** Locally cached information about folder to synchronize */
  68. private OCFile mLocalFolder;
  69. /** Counter of conflicts found between local and remote files */
  70. private int mConflictsFound;
  71. /** Counter of failed operations in synchronization of kept-in-sync files */
  72. private int mFailsInFileSyncsFound;
  73. /**
  74. * 'True' means that the remote folder changed and should be fetched
  75. */
  76. private boolean mRemoteFolderChanged;
  77. private List<OCFile> mFilesForDirectDownload;
  78. // to avoid extra PROPFINDs when there was no change in the folder
  79. private List<SyncOperation> mFilesToSyncContents;
  80. // this will be used for every file when 'folder synchronization' replaces 'folder download'
  81. private final AtomicBoolean mCancellationRequested;
  82. /**
  83. * Creates a new instance of {@link SynchronizeFolderOperation}.
  84. *
  85. * @param context Application context.
  86. * @param remotePath Path to synchronize.
  87. * @param user Nextcloud account where the folder is located.
  88. * @param currentSyncTime Time stamp for the synchronization process in progress.
  89. */
  90. public SynchronizeFolderOperation(Context context,
  91. String remotePath,
  92. User user,
  93. long currentSyncTime,
  94. FileDataStorageManager storageManager) {
  95. super(storageManager);
  96. mRemotePath = remotePath;
  97. mCurrentSyncTime = currentSyncTime;
  98. this.user = user;
  99. mContext = context;
  100. mRemoteFolderChanged = false;
  101. mFilesForDirectDownload = new Vector<>();
  102. mFilesToSyncContents = new Vector<>();
  103. mCancellationRequested = new AtomicBoolean(false);
  104. }
  105. /**
  106. * Performs the synchronization.
  107. *
  108. * {@inheritDoc}
  109. */
  110. @Override
  111. protected RemoteOperationResult run(OwnCloudClient client) {
  112. RemoteOperationResult result;
  113. mFailsInFileSyncsFound = 0;
  114. mConflictsFound = 0;
  115. try {
  116. // get locally cached information about folder
  117. mLocalFolder = getStorageManager().getFileByPath(mRemotePath);
  118. result = checkForChanges(client);
  119. if (result.isSuccess()) {
  120. if (mRemoteFolderChanged) {
  121. result = fetchAndSyncRemoteFolder(client);
  122. } else {
  123. prepareOpsFromLocalKnowledge();
  124. }
  125. if (result.isSuccess()) {
  126. syncContents();
  127. }
  128. }
  129. if (mCancellationRequested.get()) {
  130. throw new OperationCancelledException();
  131. }
  132. } catch (OperationCancelledException e) {
  133. result = new RemoteOperationResult(e);
  134. }
  135. return result;
  136. }
  137. private RemoteOperationResult checkForChanges(OwnCloudClient client) throws OperationCancelledException {
  138. Log_OC.d(TAG, "Checking changes in " + user.getAccountName() + mRemotePath);
  139. mRemoteFolderChanged = true;
  140. if (mCancellationRequested.get()) {
  141. throw new OperationCancelledException();
  142. }
  143. // remote request
  144. ReadFileRemoteOperation operation = new ReadFileRemoteOperation(mRemotePath);
  145. RemoteOperationResult result = operation.execute(client);
  146. if (result.isSuccess()) {
  147. OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0));
  148. // check if remote and local folder are different
  149. mRemoteFolderChanged = !(remoteFolder.getEtag().equalsIgnoreCase(mLocalFolder.getEtag()));
  150. result = new RemoteOperationResult(ResultCode.OK);
  151. Log_OC.i(TAG, "Checked " + user.getAccountName() + mRemotePath + " : " +
  152. (mRemoteFolderChanged ? "changed" : "not changed"));
  153. } else {
  154. // check failed
  155. if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
  156. removeLocalFolder();
  157. }
  158. if (result.isException()) {
  159. Log_OC.e(TAG, "Checked " + user.getAccountName() + mRemotePath + " : " +
  160. result.getLogMessage(), result.getException());
  161. } else {
  162. Log_OC.e(TAG, "Checked " + user.getAccountName() + mRemotePath + " : " +
  163. result.getLogMessage());
  164. }
  165. }
  166. return result;
  167. }
  168. private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) throws OperationCancelledException {
  169. if (mCancellationRequested.get()) {
  170. throw new OperationCancelledException();
  171. }
  172. ReadFolderRemoteOperation operation = new ReadFolderRemoteOperation(mRemotePath);
  173. RemoteOperationResult result = operation.execute(client);
  174. Log_OC.d(TAG, "Synchronizing " + user.getAccountName() + mRemotePath);
  175. if (result.isSuccess()) {
  176. synchronizeData(result.getData());
  177. if (mConflictsFound > 0 || mFailsInFileSyncsFound > 0) {
  178. result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
  179. // should be a different result code, but will do the job
  180. }
  181. } else {
  182. if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
  183. removeLocalFolder();
  184. }
  185. }
  186. return result;
  187. }
  188. private void removeLocalFolder() {
  189. FileDataStorageManager storageManager = getStorageManager();
  190. if (storageManager.fileExists(mLocalFolder.getFileId())) {
  191. String currentSavePath = FileStorageUtils.getSavePath(user.getAccountName());
  192. storageManager.removeFolder(
  193. mLocalFolder,
  194. true,
  195. mLocalFolder.isDown() // TODO: debug, I think this is always false for folders
  196. && mLocalFolder.getStoragePath().startsWith(currentSavePath)
  197. );
  198. }
  199. }
  200. /**
  201. * Synchronizes the data retrieved from the server about the contents of the target folder
  202. * with the current data in the local database.
  203. *
  204. * @param folderAndFiles Remote folder and children files in Folder
  205. */
  206. private void synchronizeData(List<Object> folderAndFiles) throws OperationCancelledException {
  207. // parse data from remote folder
  208. OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) folderAndFiles.get(0));
  209. remoteFolder.setParentId(mLocalFolder.getParentId());
  210. remoteFolder.setFileId(mLocalFolder.getFileId());
  211. Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() + " changed - starting update of local data ");
  212. mFilesForDirectDownload.clear();
  213. mFilesToSyncContents.clear();
  214. if (mCancellationRequested.get()) {
  215. throw new OperationCancelledException();
  216. }
  217. FileDataStorageManager storageManager = getStorageManager();
  218. // if local folder is encrypted, download fresh metadata
  219. boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(remoteFolder, storageManager);
  220. mLocalFolder.setEncrypted(encryptedAncestor);
  221. // update permission
  222. mLocalFolder.setPermissions(remoteFolder.getPermissions());
  223. // update richWorkspace
  224. mLocalFolder.setRichWorkspace(remoteFolder.getRichWorkspace());
  225. DecryptedFolderMetadata metadata = RefreshFolderOperation.getDecryptedFolderMetadata(encryptedAncestor,
  226. mLocalFolder,
  227. getClient(),
  228. user,
  229. mContext);
  230. // get current data about local contents of the folder to synchronize
  231. Map<String, OCFile> localFilesMap =
  232. RefreshFolderOperation.prefillLocalFilesMap(metadata,
  233. storageManager.getFolderContent(mLocalFolder, false));
  234. // loop to synchronize every child
  235. List<OCFile> updatedFiles = new ArrayList<>(folderAndFiles.size() - 1);
  236. OCFile remoteFile;
  237. OCFile localFile;
  238. OCFile updatedFile;
  239. RemoteFile remote;
  240. for (int i = 1; i < folderAndFiles.size(); i++) {
  241. /// new OCFile instance with the data from the server
  242. remote = (RemoteFile) folderAndFiles.get(i);
  243. remoteFile = FileStorageUtils.fillOCFile(remote);
  244. /// new OCFile instance to merge fresh data from server with local state
  245. updatedFile = FileStorageUtils.fillOCFile(remote);
  246. updatedFile.setParentId(mLocalFolder.getFileId());
  247. /// retrieve local data for the read file
  248. localFile = localFilesMap.remove(remoteFile.getRemotePath());
  249. // TODO better implementation is needed
  250. if (localFile == null) {
  251. localFile = storageManager.getFileByPath(updatedFile.getRemotePath());
  252. }
  253. /// add to updatedFile data about LOCAL STATE (not existing in server)
  254. updateLocalStateData(remoteFile, localFile, updatedFile);
  255. /// check and fix, if needed, local storage path
  256. FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, user.getAccountName());
  257. // update file name for encrypted files
  258. if (metadata != null) {
  259. RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager, metadata, updatedFile);
  260. }
  261. // we parse content, so either the folder itself or its direct parent (which we check) must be encrypted
  262. boolean encrypted = updatedFile.isEncrypted() || mLocalFolder.isEncrypted();
  263. updatedFile.setEncrypted(encrypted);
  264. /// classify file to sync/download contents later
  265. classifyFileForLaterSyncOrDownload(remoteFile, localFile);
  266. updatedFiles.add(updatedFile);
  267. }
  268. if (metadata != null) {
  269. RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager, metadata, mLocalFolder);
  270. }
  271. // save updated contents in local database
  272. storageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
  273. mLocalFolder.setLastSyncDateForData(System.currentTimeMillis());
  274. storageManager.saveFile(mLocalFolder);
  275. }
  276. private void updateLocalStateData(OCFile remoteFile, OCFile localFile, OCFile updatedFile) {
  277. updatedFile.setLastSyncDateForProperties(mCurrentSyncTime);
  278. if (localFile != null) {
  279. updatedFile.setFileId(localFile.getFileId());
  280. updatedFile.setLastSyncDateForData(localFile.getLastSyncDateForData());
  281. updatedFile.setModificationTimestampAtLastSyncForData(
  282. localFile.getModificationTimestampAtLastSyncForData()
  283. );
  284. updatedFile.setStoragePath(localFile.getStoragePath());
  285. // eTag will not be updated unless file CONTENTS are synchronized
  286. updatedFile.setEtag(localFile.getEtag());
  287. if (updatedFile.isFolder()) {
  288. updatedFile.setFileLength(localFile.getFileLength());
  289. // TODO move operations about size of folders to FileContentProvider
  290. } else if (mRemoteFolderChanged && MimeTypeUtil.isImage(remoteFile) &&
  291. remoteFile.getModificationTimestamp() !=
  292. localFile.getModificationTimestamp()) {
  293. updatedFile.setUpdateThumbnailNeeded(true);
  294. Log_OC.d(TAG, "Image " + remoteFile.getFileName() + " updated on the server");
  295. }
  296. updatedFile.setSharedViaLink(localFile.isSharedViaLink());
  297. updatedFile.setSharedWithSharee(localFile.isSharedWithSharee());
  298. updatedFile.setEtagInConflict(localFile.getEtagInConflict());
  299. } else {
  300. // remote eTag will not be updated unless file CONTENTS are synchronized
  301. updatedFile.setEtag("");
  302. }
  303. }
  304. private void classifyFileForLaterSyncOrDownload(OCFile remoteFile, OCFile localFile) {
  305. if (!remoteFile.isFolder()) {
  306. SynchronizeFileOperation operation = new SynchronizeFileOperation(
  307. localFile,
  308. remoteFile,
  309. user,
  310. true,
  311. mContext,
  312. getStorageManager()
  313. );
  314. mFilesToSyncContents.add(operation);
  315. }
  316. }
  317. private void prepareOpsFromLocalKnowledge() throws OperationCancelledException {
  318. List<OCFile> children = getStorageManager().getFolderContent(mLocalFolder, false);
  319. for (OCFile child : children) {
  320. if (!child.isFolder()) {
  321. if (!child.isDown()) {
  322. mFilesForDirectDownload.add(child);
  323. } else {
  324. /// this should result in direct upload of files that were locally modified
  325. SynchronizeFileOperation operation = new SynchronizeFileOperation(
  326. child,
  327. child.getEtagInConflict() != null ? child : null,
  328. user,
  329. true,
  330. mContext,
  331. getStorageManager()
  332. );
  333. mFilesToSyncContents.add(operation);
  334. }
  335. }
  336. }
  337. }
  338. private void syncContents() throws OperationCancelledException {
  339. startDirectDownloads();
  340. startContentSynchronizations(mFilesToSyncContents);
  341. }
  342. private void startDirectDownloads() {
  343. FileDownloadHelper.Companion.instance().downloadFile(user, mLocalFolder);
  344. }
  345. /**
  346. * Performs a list of synchronization operations, determining if a download or upload is needed
  347. * or if exists conflict due to changes both in local and remote contents of the each file.
  348. *
  349. * If download or upload is needed, request the operation to the corresponding service and goes on.
  350. *
  351. * @param filesToSyncContents Synchronization operations to execute.
  352. */
  353. private void startContentSynchronizations(List<SyncOperation> filesToSyncContents)
  354. throws OperationCancelledException {
  355. Log_OC.v(TAG, "Starting content synchronization... ");
  356. RemoteOperationResult contentsResult;
  357. for (SyncOperation op: filesToSyncContents) {
  358. if (mCancellationRequested.get()) {
  359. throw new OperationCancelledException();
  360. }
  361. contentsResult = op.execute(mContext);
  362. if (!contentsResult.isSuccess()) {
  363. if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) {
  364. mConflictsFound++;
  365. } else {
  366. mFailsInFileSyncsFound++;
  367. if (contentsResult.getException() != null) {
  368. Log_OC.e(TAG, "Error while synchronizing file : "
  369. + contentsResult.getLogMessage(), contentsResult.getException());
  370. } else {
  371. Log_OC.e(TAG, "Error while synchronizing file : "
  372. + contentsResult.getLogMessage());
  373. }
  374. }
  375. // TODO - use the errors count in notifications
  376. } // won't let these fails break the synchronization process
  377. }
  378. }
  379. /**
  380. * Scans the default location for saving local copies of files searching for
  381. * a 'lost' file with the same full name as the {@link com.owncloud.android.datamodel.OCFile}
  382. * received as parameter.
  383. *
  384. * @param file File to associate a possible 'lost' local file.
  385. */
  386. private void searchForLocalFileInDefaultPath(OCFile file) {
  387. if (file.getStoragePath() == null && !file.isFolder()) {
  388. File f = new File(FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), file));
  389. if (f.exists()) {
  390. file.setStoragePath(f.getAbsolutePath());
  391. file.setLastSyncDateForData(f.lastModified());
  392. }
  393. }
  394. }
  395. /**
  396. * Cancel operation
  397. */
  398. public void cancel() {
  399. mCancellationRequested.set(true);
  400. }
  401. public String getFolderPath() {
  402. String path = mLocalFolder.getStoragePath();
  403. if (!TextUtils.isEmpty(path)) {
  404. return path;
  405. }
  406. return FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), mLocalFolder);
  407. }
  408. private void startSyncFolderOperation(String path){
  409. Intent intent = new Intent(mContext, OperationsService.class);
  410. intent.setAction(OperationsService.ACTION_SYNC_FOLDER);
  411. intent.putExtra(OperationsService.EXTRA_ACCOUNT, user.toPlatformAccount());
  412. intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, path);
  413. mContext.startService(intent);
  414. }
  415. public String getRemotePath() {
  416. return mRemotePath;
  417. }
  418. }