local_file_update_service.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import 'dart:async';
  2. import 'dart:core';
  3. import 'dart:io';
  4. import 'package:flutter/foundation.dart';
  5. import 'package:logging/logging.dart';
  6. import 'package:photo_manager/photo_manager.dart';
  7. import 'package:photos/db/file_updation_db.dart';
  8. import 'package:photos/db/files_db.dart';
  9. import 'package:photos/utils/file_uploader_util.dart';
  10. import 'package:shared_preferences/shared_preferences.dart';
  11. // LocalFileUpdateService tracks all the potential local file IDs which have
  12. // changed/modified on the device and needed to be uploaded again.
  13. class LocalFileUpdateService {
  14. FilesDB _filesDB;
  15. FileUpdationDB _fileUpdationDB;
  16. SharedPreferences _prefs;
  17. Logger _logger;
  18. static const isLocationMigrationComplete = "fm_isLocationMigrationComplete";
  19. static const isLocalImportDone = "fm_IsLocalImportDone";
  20. Completer<void> _existingMigration;
  21. LocalFileUpdateService._privateConstructor() {
  22. _logger = Logger((LocalFileUpdateService).toString());
  23. _filesDB = FilesDB.instance;
  24. _fileUpdationDB = FileUpdationDB.instance;
  25. }
  26. Future<void> init() async {
  27. _prefs = await SharedPreferences.getInstance();
  28. }
  29. static LocalFileUpdateService instance =
  30. LocalFileUpdateService._privateConstructor();
  31. Future<bool> _markLocationMigrationAsCompleted() async {
  32. _logger.info('marking migration as completed');
  33. return _prefs.setBool(isLocationMigrationComplete, true);
  34. }
  35. bool isLocationMigrationCompleted() {
  36. return _prefs.get(isLocationMigrationComplete) ?? false;
  37. }
  38. Future<void> markUpdatedFilesForReUpload() async {
  39. if (_existingMigration != null) {
  40. _logger.info("migration is already in progress, skipping");
  41. return _existingMigration.future;
  42. }
  43. _existingMigration = Completer<void>();
  44. try {
  45. if (!isLocationMigrationCompleted() && Platform.isAndroid) {
  46. _logger.info("start migration for missing location");
  47. await _runMigrationForFilesWithMissingLocation();
  48. }
  49. await _markFilesWhichAreActuallyUpdated();
  50. _existingMigration.complete();
  51. _existingMigration = null;
  52. } catch (e, s) {
  53. _logger.severe('failed to perform migration', e, s);
  54. _existingMigration.complete();
  55. _existingMigration = null;
  56. }
  57. }
  58. // This method analyses all of local files for which the file
  59. // modification/update time was changed. It checks if the existing fileHash
  60. // is different from the hash of uploaded file. If fileHash are different,
  61. // then it marks the file for file update.
  62. Future<void> _markFilesWhichAreActuallyUpdated() async {
  63. final sTime = DateTime.now().microsecondsSinceEpoch;
  64. bool hasData = true;
  65. const int limitInBatch = 100;
  66. while (hasData) {
  67. var localIDsToProcess =
  68. await _fileUpdationDB.getLocalIDsForPotentialReUpload(
  69. limitInBatch,
  70. FileUpdationDB.modificationTimeUpdated,
  71. );
  72. if (localIDsToProcess.isEmpty) {
  73. hasData = false;
  74. } else {
  75. await _checkAndMarkFilesWithDifferentHashForFileUpdate(
  76. localIDsToProcess,
  77. );
  78. }
  79. }
  80. final eTime = DateTime.now().microsecondsSinceEpoch;
  81. final d = Duration(microseconds: eTime - sTime);
  82. _logger.info(
  83. '_markFilesWhichAreActuallyUpdated migration completed in ${d.inSeconds.toString()} seconds',
  84. );
  85. }
  86. Future<void> _checkAndMarkFilesWithDifferentHashForFileUpdate(
  87. List<String> localIDsToProcess,
  88. ) async {
  89. _logger.info("files to process ${localIDsToProcess.length} for reupload");
  90. var localFiles = await FilesDB.instance.getLocalFiles(localIDsToProcess);
  91. Set<String> processedIDs = {};
  92. for (var file in localFiles) {
  93. if (processedIDs.contains(file.localID)) {
  94. continue;
  95. }
  96. MediaUploadData uploadData;
  97. try {
  98. uploadData = await getUploadDataFromEnteFile(file);
  99. if (file.hash != null ||
  100. (file.hash == uploadData.fileHash ||
  101. file.hash == uploadData.zipHash)) {
  102. _logger.info("Skip file update as hash matched ${file.tag()}");
  103. } else {
  104. _logger.info(
  105. "Marking for file update as hash did not match ${file.tag()}",
  106. );
  107. await FilesDB.instance.updateUploadedFile(
  108. file.localID,
  109. file.title,
  110. file.location,
  111. file.creationTime,
  112. file.modificationTime,
  113. null,
  114. );
  115. }
  116. processedIDs.add(file.localID);
  117. } catch (e) {
  118. _logger.severe("Failed to get file uploadData", e);
  119. } finally {
  120. // delete the file from app's internal cache if it was copied to app
  121. // for upload. Shared Media should only be cleared when the upload
  122. // succeeds.
  123. if (Platform.isIOS &&
  124. uploadData != null &&
  125. uploadData.sourceFile != null) {
  126. await uploadData.sourceFile.delete();
  127. }
  128. }
  129. }
  130. debugPrint("Deleting files ${processedIDs.length}");
  131. await _fileUpdationDB.deleteByLocalIDs(
  132. processedIDs.toList(),
  133. FileUpdationDB.modificationTimeUpdated,
  134. );
  135. }
  136. Future<void> _runMigrationForFilesWithMissingLocation() async {
  137. if (!Platform.isAndroid) {
  138. return;
  139. }
  140. // migration only needs to run if Android API Level is 29 or higher
  141. final int version = int.parse(await PhotoManager.systemVersion());
  142. bool isMigrationRequired = version >= 29;
  143. if (isMigrationRequired) {
  144. await _importLocalFilesForMigration();
  145. final sTime = DateTime.now().microsecondsSinceEpoch;
  146. bool hasData = true;
  147. const int limitInBatch = 100;
  148. while (hasData) {
  149. var localIDsToProcess =
  150. await _fileUpdationDB.getLocalIDsForPotentialReUpload(
  151. limitInBatch,
  152. FileUpdationDB.missingLocation,
  153. );
  154. if (localIDsToProcess.isEmpty) {
  155. hasData = false;
  156. } else {
  157. await _checkAndMarkFilesWithLocationForReUpload(localIDsToProcess);
  158. }
  159. }
  160. final eTime = DateTime.now().microsecondsSinceEpoch;
  161. final d = Duration(microseconds: eTime - sTime);
  162. _logger.info(
  163. 'filesWithMissingLocation migration completed in ${d.inSeconds.toString()} seconds',
  164. );
  165. }
  166. await _markLocationMigrationAsCompleted();
  167. }
  168. Future<void> _checkAndMarkFilesWithLocationForReUpload(
  169. List<String> localIDsToProcess,
  170. ) async {
  171. _logger.info("files to process ${localIDsToProcess.length}");
  172. var localIDsWithLocation = <String>[];
  173. for (var localID in localIDsToProcess) {
  174. bool hasLocation = false;
  175. try {
  176. var assetEntity = await AssetEntity.fromId(localID);
  177. if (assetEntity == null) {
  178. continue;
  179. }
  180. var latLng = await assetEntity.latlngAsync();
  181. if ((latLng.longitude ?? 0.0) != 0.0 ||
  182. (latLng.longitude ?? 0.0) != 0.0) {
  183. _logger.finest(
  184. 'found lat/long ${latLng.longitude}/${latLng.longitude} for ${assetEntity.title} ${assetEntity.relativePath} with id : $localID',
  185. );
  186. hasLocation = true;
  187. }
  188. } catch (e, s) {
  189. _logger.severe('failed to get asset entity with id $localID', e, s);
  190. }
  191. if (hasLocation) {
  192. localIDsWithLocation.add(localID);
  193. }
  194. }
  195. _logger.info('marking ${localIDsWithLocation.length} files for re-upload');
  196. await _filesDB.markForReUploadIfLocationMissing(localIDsWithLocation);
  197. await _fileUpdationDB.deleteByLocalIDs(
  198. localIDsToProcess,
  199. FileUpdationDB.missingLocation,
  200. );
  201. }
  202. Future<void> _importLocalFilesForMigration() async {
  203. if (_prefs.containsKey(isLocalImportDone)) {
  204. return;
  205. }
  206. final sTime = DateTime.now().microsecondsSinceEpoch;
  207. _logger.info('importing files without location info');
  208. var fileLocalIDs = await _filesDB.getLocalFilesBackedUpWithoutLocation();
  209. await _fileUpdationDB.insertMultiple(
  210. fileLocalIDs,
  211. FileUpdationDB.missingLocation,
  212. );
  213. final eTime = DateTime.now().microsecondsSinceEpoch;
  214. final d = Duration(microseconds: eTime - sTime);
  215. _logger.info(
  216. 'importing completed, total files count ${fileLocalIDs.length} and took ${d.inSeconds.toString()} seconds',
  217. );
  218. await _prefs.setBool(isLocalImportDone, true);
  219. }
  220. }