remote_sync_service.dart 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:math';
  4. import 'package:flutter/foundation.dart';
  5. import 'package:flutter/widgets.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:photos/core/configuration.dart';
  8. import 'package:photos/core/errors.dart';
  9. import 'package:photos/core/event_bus.dart';
  10. import 'package:photos/db/device_files_db.dart';
  11. import 'package:photos/db/file_updation_db.dart';
  12. import 'package:photos/db/files_db.dart';
  13. import 'package:photos/events/backup_folders_updated_event.dart';
  14. import 'package:photos/events/collection_updated_event.dart';
  15. import 'package:photos/events/files_updated_event.dart';
  16. import 'package:photos/events/force_reload_home_gallery_event.dart';
  17. import 'package:photos/events/local_photos_updated_event.dart';
  18. import 'package:photos/events/sync_status_update_event.dart';
  19. import 'package:photos/models/device_collection.dart';
  20. import 'package:photos/models/file.dart';
  21. import 'package:photos/models/file_type.dart';
  22. import 'package:photos/models/upload_strategy.dart';
  23. import 'package:photos/services/app_lifecycle_service.dart';
  24. import 'package:photos/services/collections_service.dart';
  25. import 'package:photos/services/ignored_files_service.dart';
  26. import 'package:photos/services/local_file_update_service.dart';
  27. import 'package:photos/services/sync_service.dart';
  28. import 'package:photos/services/trash_sync_service.dart';
  29. import 'package:photos/utils/diff_fetcher.dart';
  30. import 'package:photos/utils/file_uploader.dart';
  31. import 'package:photos/utils/file_util.dart';
  32. import 'package:shared_preferences/shared_preferences.dart';
  33. class RemoteSyncService {
  34. final _logger = Logger("RemoteSyncService");
  35. final _db = FilesDB.instance;
  36. final FileUploader _uploader = FileUploader.instance;
  37. final Configuration _config = Configuration.instance;
  38. final CollectionsService _collectionsService = CollectionsService.instance;
  39. final DiffFetcher _diffFetcher = DiffFetcher();
  40. final LocalFileUpdateService _localFileUpdateService =
  41. LocalFileUpdateService.instance;
  42. int _completedUploads = 0;
  43. late SharedPreferences _prefs;
  44. Completer<void>? _existingSync;
  45. bool _existingSyncSilent = false;
  46. static const kHasSyncedArchiveKey = "has_synced_archive";
  47. final String _isFirstRemoteSyncDone = "isFirstRemoteSyncDone";
  48. // 28 Sept, 2021 9:03:20 AM IST
  49. static const kArchiveFeatureReleaseTime = 1632800000000000;
  50. static const kHasSyncedEditTime = "has_synced_edit_time";
  51. // 29 October, 2021 3:56:40 AM IST
  52. static const kEditTimeFeatureReleaseTime = 1635460000000000;
  53. static const kMaximumPermissibleUploadsInThrottledMode = 4;
  54. static final RemoteSyncService instance =
  55. RemoteSyncService._privateConstructor();
  56. RemoteSyncService._privateConstructor();
  57. void init(SharedPreferences preferences) {
  58. _prefs = preferences;
  59. Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) async {
  60. if (event.type == EventType.addedOrUpdated) {
  61. if (_existingSync == null) {
  62. sync();
  63. }
  64. }
  65. });
  66. }
  67. Future<void> sync({bool silently = false}) async {
  68. if (!_config.hasConfiguredAccount()) {
  69. _logger.info("Skipping remote sync since account is not configured");
  70. return;
  71. }
  72. if (_existingSync != null) {
  73. _logger.info("Remote sync already in progress, skipping");
  74. // if current sync is silent but request sync is non-silent (demands UI
  75. // updates), update the syncSilently flag
  76. if (_existingSyncSilent == true && silently == false) {
  77. _existingSyncSilent = false;
  78. }
  79. return _existingSync?.future;
  80. }
  81. _existingSync = Completer<void>();
  82. _existingSyncSilent = silently;
  83. try {
  84. // use flag to decide if we should start marking files for upload before
  85. // remote-sync is done. This is done to avoid adding existing files to
  86. // the same or different collection when user had already uploaded them
  87. // before.
  88. final bool hasSyncedBefore = _prefs.containsKey(_isFirstRemoteSyncDone);
  89. if (hasSyncedBefore) {
  90. await syncDeviceCollectionFilesForUpload();
  91. }
  92. await _pullDiff();
  93. // sync trash but consume error during initial launch.
  94. // this is to ensure that we don't pause upload due to any error during
  95. // the trash sync. Impact: We may end up re-uploading a file which was
  96. // recently trashed.
  97. await TrashSyncService.instance
  98. .syncTrash()
  99. .onError((e, s) => _logger.severe('trash sync failed', e, s));
  100. if (!hasSyncedBefore) {
  101. await _prefs.setBool(_isFirstRemoteSyncDone, true);
  102. await syncDeviceCollectionFilesForUpload();
  103. }
  104. final filesToBeUploaded = await _getFilesToBeUploaded();
  105. final hasUploadedFiles = await _uploadFiles(filesToBeUploaded);
  106. if (hasUploadedFiles) {
  107. await _pullDiff();
  108. _existingSync?.complete();
  109. _existingSync = null;
  110. await syncDeviceCollectionFilesForUpload();
  111. final hasMoreFilesToBackup = (await _getFilesToBeUploaded()).isNotEmpty;
  112. if (hasMoreFilesToBackup && !_shouldThrottleSync()) {
  113. // Skipping a resync to ensure that files that were ignored in this
  114. // session are not processed now
  115. sync();
  116. } else {
  117. debugPrint("Fire backup completed event");
  118. Bus.instance.fire(SyncStatusUpdate(SyncStatus.completedBackup));
  119. }
  120. } else {
  121. _existingSync?.complete();
  122. _existingSync = null;
  123. }
  124. } catch (e, s) {
  125. _existingSync?.complete();
  126. _existingSync = null;
  127. // rethrow whitelisted error so that UI status can be updated correctly.
  128. if (e is UnauthorizedError ||
  129. e is NoActiveSubscriptionError ||
  130. e is WiFiUnavailableError ||
  131. e is StorageLimitExceededError ||
  132. e is SyncStopRequestedError) {
  133. _logger.warning("Error executing remote sync", e);
  134. rethrow;
  135. } else {
  136. _logger.severe("Error executing remote sync ", e, s);
  137. }
  138. } finally {
  139. _existingSyncSilent = false;
  140. }
  141. }
  142. Future<void> _pullDiff() async {
  143. final isFirstSync = !_collectionsService.hasSyncedCollections();
  144. await _collectionsService.sync();
  145. // check and reset user's collection syncTime in past for older clients
  146. if (isFirstSync) {
  147. // not need reset syncTime, mark all flags as done if firstSync
  148. await _markResetSyncTimeAsDone();
  149. } else if (_shouldResetSyncTime()) {
  150. _logger.warning('Resetting syncTime for for the client');
  151. await _resetAllCollectionsSyncTime();
  152. await _markResetSyncTimeAsDone();
  153. }
  154. await _syncUpdatedCollections();
  155. unawaited(_localFileUpdateService.markUpdatedFilesForReUpload());
  156. }
  157. Future<void> _syncUpdatedCollections() async {
  158. final updatedCollections =
  159. await _collectionsService.getCollectionsToBeSynced();
  160. for (final c in updatedCollections) {
  161. await _syncCollectionDiff(
  162. c.id,
  163. _collectionsService.getCollectionSyncTime(c.id),
  164. );
  165. await _collectionsService.setCollectionSyncTime(c.id, c.updationTime);
  166. }
  167. }
  168. Future<void> _resetAllCollectionsSyncTime() async {
  169. final resetSyncTime = _getSinceTimeForReSync();
  170. _logger.info('re-setting all collections syncTime to: $resetSyncTime');
  171. final collections = _collectionsService.getActiveCollections();
  172. for (final c in collections) {
  173. final int newSyncTime =
  174. min(_collectionsService.getCollectionSyncTime(c.id), resetSyncTime);
  175. await _collectionsService.setCollectionSyncTime(c.id, newSyncTime);
  176. }
  177. }
  178. Future<void> _syncCollectionDiff(int collectionID, int sinceTime) async {
  179. if (!_existingSyncSilent) {
  180. Bus.instance.fire(SyncStatusUpdate(SyncStatus.applyingRemoteDiff));
  181. }
  182. final diff =
  183. await _diffFetcher.getEncryptedFilesDiff(collectionID, sinceTime);
  184. if (diff.deletedFiles.isNotEmpty) {
  185. final fileIDs = diff.deletedFiles.map((f) => f.uploadedFileID!).toList();
  186. final deletedFiles = (await _db.getFilesFromIDs(fileIDs)).values.toList();
  187. await _db.deleteFilesFromCollection(collectionID, fileIDs);
  188. Bus.instance.fire(
  189. CollectionUpdatedEvent(
  190. collectionID,
  191. deletedFiles,
  192. "syncDeleteFromRemote",
  193. type: EventType.deletedFromRemote,
  194. ),
  195. );
  196. Bus.instance.fire(
  197. LocalPhotosUpdatedEvent(
  198. deletedFiles,
  199. type: EventType.deletedFromRemote,
  200. source: "syncDeleteFromRemote",
  201. ),
  202. );
  203. }
  204. if (diff.updatedFiles.isNotEmpty) {
  205. await _storeDiff(diff.updatedFiles, collectionID);
  206. _logger.info(
  207. "Updated " +
  208. diff.updatedFiles.length.toString() +
  209. " files in collection " +
  210. collectionID.toString(),
  211. );
  212. Bus.instance.fire(
  213. LocalPhotosUpdatedEvent(
  214. diff.updatedFiles,
  215. source: "syncUpdateFromRemote",
  216. ),
  217. );
  218. Bus.instance.fire(
  219. CollectionUpdatedEvent(
  220. collectionID,
  221. diff.updatedFiles,
  222. "syncUpdateFromRemote",
  223. ),
  224. );
  225. }
  226. if (diff.latestUpdatedAtTime > 0) {
  227. await _collectionsService.setCollectionSyncTime(
  228. collectionID,
  229. diff.latestUpdatedAtTime,
  230. );
  231. }
  232. if (diff.hasMore) {
  233. return await _syncCollectionDiff(
  234. collectionID,
  235. _collectionsService.getCollectionSyncTime(collectionID),
  236. );
  237. }
  238. }
  239. Future<void> syncDeviceCollectionFilesForUpload() async {
  240. final int ownerID = _config.getUserID()!;
  241. final deviceCollections = await _db.getDeviceCollections();
  242. deviceCollections.removeWhere((element) => !element.shouldBackup);
  243. // Sort by count to ensure that photos in iOS are first inserted in
  244. // smallest album marked for backup. This is to ensure that photo is
  245. // first attempted to upload in a non-recent album.
  246. deviceCollections.sort((a, b) => a.count.compareTo(b.count));
  247. final Map<String, Set<String>> pathIdToLocalIDs =
  248. await _db.getDevicePathIDToLocalIDMap();
  249. bool moreFilesMarkedForBackup = false;
  250. for (final deviceCollection in deviceCollections) {
  251. final Set<String> localIDsToSync =
  252. pathIdToLocalIDs[deviceCollection.id] ?? {};
  253. if (deviceCollection.uploadStrategy == UploadStrategy.ifMissing) {
  254. final Set<String> alreadyClaimedLocalIDs =
  255. await _db.getLocalIDsMarkedForOrAlreadyUploaded(ownerID);
  256. localIDsToSync.removeAll(alreadyClaimedLocalIDs);
  257. }
  258. if (localIDsToSync.isEmpty) {
  259. continue;
  260. }
  261. await _createCollectionForDevicePath(deviceCollection);
  262. if (deviceCollection.collectionID == -1) {
  263. _logger.finest('DeviceCollection should not be -1 here');
  264. continue;
  265. }
  266. moreFilesMarkedForBackup = true;
  267. await _db.setCollectionIDForUnMappedLocalFiles(
  268. deviceCollection.collectionID!,
  269. localIDsToSync,
  270. );
  271. // mark IDs as already synced if corresponding entry is present in
  272. // the collection. This can happen when a user has marked a folder
  273. // for sync, then un-synced it and again tries to mark if for sync.
  274. final Set<String> existingMapping =
  275. await _db.getLocalFileIDsForCollection(
  276. deviceCollection.collectionID!,
  277. );
  278. final Set<String> commonElements =
  279. localIDsToSync.intersection(existingMapping);
  280. if (commonElements.isNotEmpty) {
  281. debugPrint(
  282. "${commonElements.length} files already existing in "
  283. "collection ${deviceCollection.collectionID} for ${deviceCollection.name}",
  284. );
  285. localIDsToSync.removeAll(commonElements);
  286. }
  287. // At this point, the remaining localIDsToSync will need to create
  288. // new file entries, where we can store mapping for localID and
  289. // corresponding collection ID
  290. if (localIDsToSync.isNotEmpty) {
  291. debugPrint(
  292. 'Adding new entries for ${localIDsToSync.length} files'
  293. ' for ${deviceCollection.name}',
  294. );
  295. final filesWithCollectionID =
  296. await _db.getLocalFiles(localIDsToSync.toList());
  297. final List<File> newFilesToInsert = [];
  298. final Set<String> fileFoundForLocalIDs = {};
  299. for (var existingFile in filesWithCollectionID) {
  300. final String localID = existingFile.localID!;
  301. if (!fileFoundForLocalIDs.contains(localID)) {
  302. existingFile.generatedID = null;
  303. existingFile.collectionID = deviceCollection.collectionID;
  304. existingFile.uploadedFileID = null;
  305. existingFile.ownerID = null;
  306. newFilesToInsert.add(existingFile);
  307. fileFoundForLocalIDs.add(localID);
  308. }
  309. }
  310. await _db.insertMultiple(newFilesToInsert);
  311. if (fileFoundForLocalIDs.length != localIDsToSync.length) {
  312. _logger.warning(
  313. "mismatch in num of filesToSync ${localIDsToSync.length} to "
  314. "fileSynced ${fileFoundForLocalIDs.length}",
  315. );
  316. }
  317. }
  318. }
  319. if (moreFilesMarkedForBackup && !_config.hasSelectedAllFoldersForBackup()) {
  320. // "force reload due to display new files"
  321. Bus.instance.fire(ForceReloadHomeGalleryEvent("newFilesDisplay"));
  322. }
  323. }
  324. Future<void> updateDeviceFolderSyncStatus(
  325. Map<String, bool> syncStatusUpdate,
  326. ) async {
  327. final Set<int> oldCollectionIDsForAutoSync =
  328. await _db.getDeviceSyncCollectionIDs();
  329. await _db.updateDevicePathSyncStatus(syncStatusUpdate);
  330. final Set<int> newCollectionIDsForAutoSync =
  331. await _db.getDeviceSyncCollectionIDs();
  332. SyncService.instance.onDeviceCollectionSet(newCollectionIDsForAutoSync);
  333. // remove all collectionIDs which are still marked for backup
  334. oldCollectionIDsForAutoSync.removeAll(newCollectionIDsForAutoSync);
  335. await removeFilesQueuedForUpload(oldCollectionIDsForAutoSync.toList());
  336. if (syncStatusUpdate.values.any((syncStatus) => syncStatus == false)) {
  337. Configuration.instance.setSelectAllFoldersForBackup(false).ignore();
  338. }
  339. Bus.instance.fire(
  340. LocalPhotosUpdatedEvent(<File>[], source: "deviceFolderSync"),
  341. );
  342. Bus.instance.fire(BackupFoldersUpdatedEvent());
  343. }
  344. Future<void> removeFilesQueuedForUpload(List<int> collectionIDs) async {
  345. /*
  346. For each collection, perform following action
  347. 1) Get List of all files not uploaded yet
  348. 2) Delete files who localIDs is also present in other collections.
  349. 3) For Remaining files, set the collectionID as -1
  350. */
  351. _logger.info("Removing files for collections $collectionIDs");
  352. for (int collectionID in collectionIDs) {
  353. final List<File> pendingUploads =
  354. await _db.getPendingUploadForCollection(collectionID);
  355. if (pendingUploads.isEmpty) {
  356. continue;
  357. } else {
  358. _logger.info(
  359. "RemovingFiles $collectionIDs: pendingUploads "
  360. "${pendingUploads.length}",
  361. );
  362. }
  363. final Set<String> localIDsInOtherFileEntries =
  364. await _db.getLocalIDsPresentInEntries(
  365. pendingUploads,
  366. collectionID,
  367. );
  368. _logger.info(
  369. "RemovingFiles $collectionIDs: filesInOtherCollection "
  370. "${localIDsInOtherFileEntries.length}",
  371. );
  372. final List<File> entriesToUpdate = [];
  373. final List<int> entriesToDelete = [];
  374. for (File pendingUpload in pendingUploads) {
  375. if (localIDsInOtherFileEntries.contains(pendingUpload.localID)) {
  376. entriesToDelete.add(pendingUpload.generatedID!);
  377. } else {
  378. pendingUpload.collectionID = null;
  379. entriesToUpdate.add(pendingUpload);
  380. }
  381. }
  382. await _db.deleteMultipleByGeneratedIDs(entriesToDelete);
  383. await _db.insertMultiple(entriesToUpdate);
  384. _logger.info(
  385. "RemovingFiles $collectionIDs: deleted "
  386. "${entriesToDelete.length} and updated ${entriesToUpdate.length}",
  387. );
  388. }
  389. }
  390. Future<void> _createCollectionForDevicePath(
  391. DeviceCollection deviceCollection,
  392. ) async {
  393. int deviceCollectionID = deviceCollection.collectionID ?? -1;
  394. if (deviceCollectionID != -1) {
  395. final collectionByID =
  396. _collectionsService.getCollectionByID(deviceCollectionID);
  397. if (collectionByID == null || collectionByID.isDeleted) {
  398. _logger.info(
  399. "Collection $deviceCollectionID either deleted or missing "
  400. "for path ${deviceCollection.id}",
  401. );
  402. deviceCollectionID = -1;
  403. }
  404. }
  405. if (deviceCollectionID == -1) {
  406. final collection =
  407. await _collectionsService.getOrCreateForPath(deviceCollection.name);
  408. await _db.updateDeviceCollection(deviceCollection.id, collection.id);
  409. deviceCollection.collectionID = collection.id;
  410. }
  411. }
  412. Future<List<File>> _getFilesToBeUploaded() async {
  413. final deviceCollections = await _db.getDeviceCollections();
  414. deviceCollections.removeWhere((element) => !element.shouldBackup);
  415. final List<File> filesToBeUploaded = await _db.getFilesPendingForUpload();
  416. if (!_config.shouldBackupVideos() || _shouldThrottleSync()) {
  417. filesToBeUploaded
  418. .removeWhere((element) => element.fileType == FileType.video);
  419. }
  420. if (filesToBeUploaded.isNotEmpty) {
  421. final int prevCount = filesToBeUploaded.length;
  422. final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
  423. filesToBeUploaded.removeWhere(
  424. (file) =>
  425. IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, file),
  426. );
  427. if (prevCount != filesToBeUploaded.length) {
  428. _logger.info(
  429. (prevCount - filesToBeUploaded.length).toString() +
  430. " files were ignored for upload",
  431. );
  432. }
  433. }
  434. _sortByTimeAndType(filesToBeUploaded);
  435. _logger.info(
  436. filesToBeUploaded.length.toString() + " new files to be uploaded.",
  437. );
  438. return filesToBeUploaded;
  439. }
  440. Future<bool> _uploadFiles(List<File> filesToBeUploaded) async {
  441. final int ownerID = _config.getUserID()!;
  442. final updatedFileIDs = await _db.getUploadedFileIDsToBeUpdated(ownerID);
  443. if (updatedFileIDs.isNotEmpty) {
  444. _logger.info("Identified ${updatedFileIDs.length} files for reupload");
  445. }
  446. _completedUploads = 0;
  447. final int toBeUploaded = filesToBeUploaded.length + updatedFileIDs.length;
  448. if (toBeUploaded > 0) {
  449. Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparingForUpload));
  450. // verify if files upload is allowed based on their subscription plan and
  451. // storage limit. To avoid creating new endpoint, we are using
  452. // fetchUploadUrls as alternative method.
  453. await _uploader.fetchUploadURLs(toBeUploaded);
  454. }
  455. final List<Future> futures = [];
  456. for (final uploadedFileID in updatedFileIDs) {
  457. if (_shouldThrottleSync() &&
  458. futures.length >= kMaximumPermissibleUploadsInThrottledMode) {
  459. _logger
  460. .info("Skipping some updated files as we are throttling uploads");
  461. break;
  462. }
  463. final file = await _db.getUploadedFileInAnyCollection(uploadedFileID);
  464. if (file != null) {
  465. _uploadFile(file, file.collectionID!, futures);
  466. }
  467. }
  468. for (final file in filesToBeUploaded) {
  469. if (_shouldThrottleSync() &&
  470. futures.length >= kMaximumPermissibleUploadsInThrottledMode) {
  471. _logger.info("Skipping some new files as we are throttling uploads");
  472. break;
  473. }
  474. // prefer existing collection ID for manually uploaded files.
  475. // See https://github.com/ente-io/photos-app/pull/187
  476. final collectionID = file.collectionID ??
  477. (await _collectionsService
  478. .getOrCreateForPath(file.deviceFolder ?? 'Unknown Folder'))
  479. .id;
  480. _uploadFile(file, collectionID, futures);
  481. }
  482. try {
  483. await Future.wait(futures);
  484. } on InvalidFileError {
  485. // Do nothing
  486. } on FileSystemException {
  487. // Do nothing since it's caused mostly due to concurrency issues
  488. // when the foreground app deletes temporary files, interrupting a background
  489. // upload
  490. } on LockAlreadyAcquiredError {
  491. // Do nothing
  492. } on SilentlyCancelUploadsError {
  493. // Do nothing
  494. } on UserCancelledUploadError {
  495. // Do nothing
  496. } catch (e) {
  497. rethrow;
  498. }
  499. return _completedUploads > 0;
  500. }
  501. void _uploadFile(File file, int collectionID, List<Future> futures) {
  502. final future = _uploader
  503. .upload(file, collectionID)
  504. .then((uploadedFile) => _onFileUploaded(uploadedFile));
  505. futures.add(future);
  506. }
  507. Future<void> _onFileUploaded(File file) async {
  508. Bus.instance.fire(
  509. CollectionUpdatedEvent(file.collectionID, [file], "fileUpload"),
  510. );
  511. _completedUploads++;
  512. final toBeUploadedInThisSession = _uploader.getCurrentSessionUploadCount();
  513. if (toBeUploadedInThisSession == 0) {
  514. return;
  515. }
  516. if (_completedUploads > toBeUploadedInThisSession ||
  517. _completedUploads < 0 ||
  518. toBeUploadedInThisSession < 0) {
  519. _logger.info(
  520. "Incorrect sync status",
  521. InvalidSyncStatusError(
  522. "Tried to report $_completedUploads as "
  523. "uploaded out of $toBeUploadedInThisSession",
  524. ),
  525. );
  526. return;
  527. }
  528. Bus.instance.fire(
  529. SyncStatusUpdate(
  530. SyncStatus.inProgress,
  531. completed: _completedUploads,
  532. total: toBeUploadedInThisSession,
  533. ),
  534. );
  535. }
  536. /* _storeDiff maps each remoteDiff file to existing
  537. entries in files table. When match is found, it compares both file to
  538. perform relevant actions like
  539. [1] Clear local cache when required (Both Shared and Owned files)
  540. [2] Retain localID of remote file based on matching logic [Owned files]
  541. [3] Refresh UI if visibility or creationTime has changed [Owned files]
  542. [4] Schedule file update if the local file has changed since last time
  543. [Owned files]
  544. [Important Note: If given uploadedFileID and collectionID is already present
  545. in files db, the generateID should already point to existing entry.
  546. Known Issues:
  547. [K1] Cached entry will not be cleared when if a file was edited and
  548. moved to different collection as Vid/Image cache key is uploadedID.
  549. [Existing]
  550. ]
  551. */
  552. Future _storeDiff(List<File> diff, int collectionID) async {
  553. int sharedFileNew = 0,
  554. sharedFileUpdated = 0,
  555. localUploadedFromDevice = 0,
  556. localButUpdatedOnDevice = 0,
  557. remoteNewFile = 0;
  558. final int userID = _config.getUserID()!;
  559. bool needsGalleryReload = false;
  560. // this is required when same file is uploaded twice in the same
  561. // collection. Without this check, if both remote files are part of same
  562. // diff response, then we end up inserting one entry instead of two
  563. // as we update the generatedID for remoteDiff to local file's genID
  564. final Set<int> alreadyClaimedLocalFilesGenID = {};
  565. final List<File> toBeInserted = [];
  566. for (File remoteDiff in diff) {
  567. // existingFile will be either set to existing collectionID+localID or
  568. // to the unclaimed aka not already linked to any uploaded file.
  569. File? existingFile;
  570. if (remoteDiff.generatedID != null) {
  571. // Case [1] Check and clear local cache when uploadedFile already exist
  572. // Note: Existing file can be null here if it's replaced by the time we
  573. // reach here
  574. existingFile = await _db.getFile(remoteDiff.generatedID!);
  575. if (existingFile != null &&
  576. _shouldClearCache(remoteDiff, existingFile)) {
  577. needsGalleryReload = true;
  578. await clearCache(remoteDiff);
  579. }
  580. }
  581. /* If file is not owned by the user, no further processing is required
  582. as Case [2,3,4] are only relevant to files owned by user
  583. */
  584. if (userID != remoteDiff.ownerID) {
  585. if (existingFile == null) {
  586. sharedFileNew++;
  587. remoteDiff.localID = null;
  588. } else {
  589. sharedFileUpdated++;
  590. // if user has downloaded the file on the device, avoid removing the
  591. // localID reference.
  592. // [Todo-fix: Excluded shared file's localIDs during syncALL]
  593. remoteDiff.localID = existingFile.localID;
  594. }
  595. toBeInserted.add(remoteDiff);
  596. // end processing for file here, move to next file now
  597. continue;
  598. }
  599. // If remoteDiff is not already synced (i.e. existingFile is null), check
  600. // if the remoteFile was uploaded from this device.
  601. // Note: DeviceFolder is ignored for iOS during matching
  602. if (existingFile == null && remoteDiff.localID != null) {
  603. final localFileEntries = await _db.getUnlinkedLocalMatchesForRemoteFile(
  604. userID,
  605. remoteDiff.localID!,
  606. remoteDiff.fileType,
  607. title: remoteDiff.title ?? '',
  608. deviceFolder: remoteDiff.deviceFolder ?? '',
  609. );
  610. if (localFileEntries.isEmpty) {
  611. // set remote file's localID as null because corresponding local file
  612. // does not exist [Case 2, do not retain localID of the remote file]
  613. remoteDiff.localID = null;
  614. } else {
  615. // case 4: Check and schedule the file for update
  616. final int maxModificationTime = localFileEntries
  617. .map(
  618. (e) => e.modificationTime ?? 0,
  619. )
  620. .reduce(max);
  621. /* Note: In case of iOS, we will miss any asset modification in
  622. between of two installation. This is done to avoid fetching assets
  623. from iCloud when modification time could have changed for number of
  624. reasons. To fix this, we need to identify a way to store version
  625. for the adjustments or just if the asset has been modified ever.
  626. https://stackoverflow.com/a/50093266/546896
  627. */
  628. if (maxModificationTime > remoteDiff.modificationTime! &&
  629. Platform.isAndroid) {
  630. localButUpdatedOnDevice++;
  631. await FileUpdationDB.instance.insertMultiple(
  632. [remoteDiff.localID!],
  633. FileUpdationDB.modificationTimeUpdated,
  634. );
  635. }
  636. localFileEntries.removeWhere(
  637. (e) =>
  638. e.uploadedFileID != null ||
  639. alreadyClaimedLocalFilesGenID.contains(e.generatedID),
  640. );
  641. if (localFileEntries.isNotEmpty) {
  642. // file uploaded from same device, replace the local file row by
  643. // setting the generated ID of remoteFile to localFile generatedID
  644. existingFile = localFileEntries.first;
  645. localUploadedFromDevice++;
  646. alreadyClaimedLocalFilesGenID.add(existingFile.generatedID!);
  647. remoteDiff.generatedID = existingFile.generatedID;
  648. }
  649. }
  650. }
  651. if (existingFile != null &&
  652. _shouldReloadHomeGallery(remoteDiff, existingFile)) {
  653. needsGalleryReload = true;
  654. } else {
  655. remoteNewFile++;
  656. }
  657. toBeInserted.add(remoteDiff);
  658. }
  659. await _db.insertMultiple(toBeInserted);
  660. _logger.info(
  661. "Diff to be deduplicated was: " +
  662. diff.length.toString() +
  663. " out of which \n" +
  664. localUploadedFromDevice.toString() +
  665. " was uploaded from device, \n" +
  666. localButUpdatedOnDevice.toString() +
  667. " was uploaded from device, but has been updated since and should be reuploaded, \n" +
  668. sharedFileNew.toString() +
  669. " new sharedFiles, \n" +
  670. sharedFileUpdated.toString() +
  671. " updatedSharedFiles, and \n" +
  672. remoteNewFile.toString() +
  673. " remoteFiles seen first time",
  674. );
  675. if (needsGalleryReload) {
  676. // 'force reload home gallery'
  677. Bus.instance.fire(ForceReloadHomeGalleryEvent("remoteSync"));
  678. }
  679. }
  680. bool _shouldClearCache(File remoteFile, File existingFile) {
  681. if (remoteFile.hash != null && existingFile.hash != null) {
  682. return remoteFile.hash != existingFile.hash;
  683. }
  684. return remoteFile.updationTime != (existingFile.updationTime ?? 0);
  685. }
  686. bool _shouldReloadHomeGallery(File remoteFile, File existingFile) {
  687. int remoteCreationTime = remoteFile.creationTime!;
  688. if (remoteFile.pubMmdVersion > 0 &&
  689. (remoteFile.pubMagicMetadata?.editedTime ?? 0) != 0) {
  690. remoteCreationTime = remoteFile.pubMagicMetadata!.editedTime!;
  691. }
  692. if (remoteCreationTime != existingFile.creationTime) {
  693. return true;
  694. }
  695. if (existingFile.mMdVersion > 0 &&
  696. remoteFile.mMdVersion != existingFile.mMdVersion &&
  697. remoteFile.magicMetadata.visibility !=
  698. existingFile.magicMetadata.visibility) {
  699. return false;
  700. }
  701. return false;
  702. }
  703. // return true if the client needs to re-sync the collections from previous
  704. // version
  705. bool _shouldResetSyncTime() {
  706. return !_prefs.containsKey(kHasSyncedEditTime) ||
  707. !_prefs.containsKey(kHasSyncedArchiveKey);
  708. }
  709. Future<void> _markResetSyncTimeAsDone() async {
  710. await _prefs.setBool(kHasSyncedArchiveKey, true);
  711. await _prefs.setBool(kHasSyncedEditTime, true);
  712. // Check to avoid regression because of change or additions of keys
  713. if (_shouldResetSyncTime()) {
  714. throw Exception("_shouldResetSyncTime should return false now");
  715. }
  716. }
  717. int _getSinceTimeForReSync() {
  718. // re-sync from archive feature time if the client still hasn't synced
  719. // since the feature release.
  720. if (!_prefs.containsKey(kHasSyncedArchiveKey)) {
  721. return kArchiveFeatureReleaseTime;
  722. }
  723. return kEditTimeFeatureReleaseTime;
  724. }
  725. bool _shouldThrottleSync() {
  726. return Platform.isIOS && !AppLifecycleService.instance.isForeground;
  727. }
  728. // _sortByTimeAndType moves videos to end and sort by creation time (desc).
  729. // This is done to upload most recent photo first.
  730. void _sortByTimeAndType(List<File> file) {
  731. file.sort((first, second) {
  732. if (first.fileType == second.fileType) {
  733. return second.creationTime!.compareTo(first.creationTime!);
  734. } else if (first.fileType == FileType.video) {
  735. return 1;
  736. } else {
  737. return -1;
  738. }
  739. });
  740. // move updated files towards the end
  741. file.sort((first, second) {
  742. if (first.updationTime == second.updationTime) {
  743. return 0;
  744. }
  745. if (first.updationTime == -1) {
  746. return 1;
  747. } else {
  748. return -1;
  749. }
  750. });
  751. }
  752. }