local_file_update_service.dart 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import 'dart:async';
  2. import 'dart:core';
  3. import 'dart:io';
  4. import 'package:logging/logging.dart';
  5. import "package:photos/core/configuration.dart";
  6. import 'package:photos/core/errors.dart';
  7. import 'package:photos/db/file_updation_db.dart';
  8. import 'package:photos/db/files_db.dart';
  9. import 'package:photos/models/file.dart';
  10. import "package:photos/models/file_type.dart";
  11. import 'package:photos/utils/file_uploader_util.dart';
  12. import 'package:photos/utils/file_util.dart';
  13. import 'package:shared_preferences/shared_preferences.dart';
  14. // LocalFileUpdateService tracks all the potential local file IDs which have
  15. // changed/modified on the device and needed to be uploaded again.
  16. class LocalFileUpdateService {
  17. late FileUpdationDB _fileUpdationDB;
  18. late SharedPreferences _prefs;
  19. late Logger _logger;
  20. final List<String> _oldMigrationKeys = [
  21. 'fm_badCreationTime',
  22. 'fm_badCreationTimeCompleted',
  23. 'fm_missingLocationV2ImportDone',
  24. 'fm_missingLocationV2MigrationDone',
  25. 'fm_badLocationImportDone',
  26. 'fm_badLocationMigrationDone',
  27. ];
  28. Completer<void>? _existingMigration;
  29. LocalFileUpdateService._privateConstructor() {
  30. _logger = Logger((LocalFileUpdateService).toString());
  31. _fileUpdationDB = FileUpdationDB.instance;
  32. }
  33. void init(SharedPreferences preferences) {
  34. _prefs = preferences;
  35. }
  36. static LocalFileUpdateService instance =
  37. LocalFileUpdateService._privateConstructor();
  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. await _markFilesWhichAreActuallyUpdated();
  46. if (Platform.isAndroid) {
  47. _cleanUpOlderMigration().ignore();
  48. }
  49. } catch (e, s) {
  50. _logger.severe('failed to perform migration', e, s);
  51. } finally {
  52. _existingMigration?.complete();
  53. _existingMigration = null;
  54. }
  55. }
  56. Future<void> _cleanUpOlderMigration() async {
  57. // check if any old_migration_keys are present in shared preferences
  58. bool hasOldMigrationKey = false;
  59. for (String key in _oldMigrationKeys) {
  60. if (_prefs.containsKey(key)) {
  61. hasOldMigrationKey = true;
  62. break;
  63. }
  64. }
  65. if (hasOldMigrationKey) {
  66. for (var element in _oldMigrationKeys) {
  67. _prefs.remove(element);
  68. }
  69. await _fileUpdationDB.deleteByReasons([
  70. 'missing_location',
  71. 'badCreationTime',
  72. 'missingLocationV2',
  73. 'badLocationCord',
  74. ]);
  75. }
  76. }
  77. // This method analyses all of local files for which the file
  78. // modification/update time was changed. It checks if the existing fileHash
  79. // is different from the hash of uploaded file. If fileHash are different,
  80. // then it marks the file for file update.
  81. Future<void> _markFilesWhichAreActuallyUpdated() async {
  82. final sTime = DateTime.now().microsecondsSinceEpoch;
  83. // singleRunLimit indicates number of files to check during single
  84. // invocation of this method. The limit act as a crude way to limit the
  85. // resource consumed by the method
  86. const int singleRunLimit = 10;
  87. final localIDsToProcess =
  88. await _fileUpdationDB.getLocalIDsForPotentialReUpload(
  89. singleRunLimit,
  90. FileUpdationDB.modificationTimeUpdated,
  91. );
  92. if (localIDsToProcess.isNotEmpty) {
  93. await _checkAndMarkFilesWithDifferentHashForFileUpdate(
  94. localIDsToProcess,
  95. );
  96. final eTime = DateTime.now().microsecondsSinceEpoch;
  97. final d = Duration(microseconds: eTime - sTime);
  98. _logger.info(
  99. 'Performed hashCheck for ${localIDsToProcess.length} updated files '
  100. 'completed in ${d.inSeconds.toString()} secs',
  101. );
  102. }
  103. }
  104. Future<void> _checkAndMarkFilesWithDifferentHashForFileUpdate(
  105. List<String> localIDsToProcess,
  106. ) async {
  107. _logger.info("files to process ${localIDsToProcess.length} for reupload");
  108. final int userID = Configuration.instance.getUserID()!;
  109. final List<EnteFile> result =
  110. await FilesDB.instance.getLocalFiles(localIDsToProcess);
  111. final List<EnteFile> localFilesForUser = [];
  112. for (EnteFile file in result) {
  113. if (file.ownerID == null || file.ownerID == userID) {
  114. localFilesForUser.add(file);
  115. }
  116. }
  117. final Set<String> processedIDs = {};
  118. for (EnteFile file in localFilesForUser) {
  119. if (processedIDs.contains(file.localID)) {
  120. continue;
  121. }
  122. MediaUploadData uploadData;
  123. try {
  124. uploadData = await getUploadData(file);
  125. if (uploadData.hashData != null &&
  126. file.hash != null &&
  127. (file.hash == uploadData.hashData!.fileHash ||
  128. file.hash == uploadData.hashData!.zipHash)) {
  129. _logger.info("Skip file update as hash matched ${file.tag}");
  130. } else {
  131. _logger.info(
  132. "Marking for file update as hash did not match ${file.tag}",
  133. );
  134. await clearCache(file);
  135. await FilesDB.instance.markFilesForReUpload(
  136. userID,
  137. file.localID!,
  138. file.title,
  139. file.location,
  140. file.creationTime!,
  141. file.modificationTime!,
  142. file.fileType,
  143. );
  144. }
  145. processedIDs.add(file.localID!);
  146. } on InvalidFileError catch (e) {
  147. if (e.reason == InvalidReason.livePhotoToImageTypeChanged ||
  148. e.reason == InvalidReason.imageToLivePhotoTypeChanged) {
  149. late FileType fileType;
  150. if (e.reason == InvalidReason.livePhotoToImageTypeChanged) {
  151. fileType = FileType.image;
  152. } else if (e.reason == InvalidReason.imageToLivePhotoTypeChanged) {
  153. fileType = FileType.livePhoto;
  154. }
  155. final int count = await FilesDB.instance.markFilesForReUpload(
  156. userID,
  157. file.localID!,
  158. file.title,
  159. file.location,
  160. file.creationTime!,
  161. file.modificationTime!,
  162. fileType,
  163. );
  164. _logger.fine('fileType changed for ${file.tag} to ${e.reason} for '
  165. '$count files');
  166. } else {
  167. _logger.severe("failed to check hash: invalid file ${file.tag}", e);
  168. }
  169. processedIDs.add(file.localID!);
  170. } catch (e) {
  171. _logger.severe("Failed to check hash", e);
  172. } finally {}
  173. }
  174. await _fileUpdationDB.deleteByLocalIDs(
  175. processedIDs.toList(),
  176. FileUpdationDB.modificationTimeUpdated,
  177. );
  178. }
  179. Future<MediaUploadData> getUploadData(EnteFile file) async {
  180. final mediaUploadData = await getUploadDataFromEnteFile(file);
  181. // delete the file from app's internal cache if it was copied to app
  182. // for upload. Shared Media should only be cleared when the upload
  183. // succeeds.
  184. if (Platform.isIOS && mediaUploadData.sourceFile != null) {
  185. await mediaUploadData.sourceFile?.delete();
  186. }
  187. return mediaUploadData;
  188. }
  189. }