local_file_update_service.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import 'dart:async';
  2. import 'dart:core';
  3. import 'dart:io';
  4. import "package:collection/collection.dart";
  5. import 'package:flutter/foundation.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:photos/core/configuration.dart';
  8. import 'package:photos/core/constants.dart';
  9. import 'package:photos/core/errors.dart';
  10. import 'package:photos/db/file_updation_db.dart';
  11. import 'package:photos/db/files_db.dart';
  12. import 'package:photos/extensions/stop_watch.dart';
  13. import 'package:photos/models/file.dart' as ente;
  14. import "package:photos/models/location/location.dart";
  15. import "package:photos/models/magic_metadata.dart";
  16. import "package:photos/services/file_magic_service.dart";
  17. import 'package:photos/services/files_service.dart';
  18. import "package:photos/utils/exif_util.dart";
  19. import 'package:photos/utils/file_uploader_util.dart';
  20. import 'package:photos/utils/file_util.dart';
  21. import 'package:shared_preferences/shared_preferences.dart';
  22. // LocalFileUpdateService tracks all the potential local file IDs which have
  23. // changed/modified on the device and needed to be uploaded again.
  24. class LocalFileUpdateService {
  25. late FileUpdationDB _fileUpdationDB;
  26. late SharedPreferences _prefs;
  27. late Logger _logger;
  28. static const isBadCreationTimeImportDone = 'fm_badCreationTime';
  29. static const isBadCreationTimeMigrationComplete =
  30. 'fm_badCreationTimeCompleted';
  31. static const isMissingLocationV2ImportDone = "fm_missingLocationV2ImportDone";
  32. static const isMissingLocationV2MigrationDone =
  33. "fm_missingLocationV2MigrationDone";
  34. static const isBadLocationCordImportDone = "fm_badLocationImportDone";
  35. static const isBadLocationCordMigrationDone = "fm_badLocationMigrationDone";
  36. Completer<void>? _existingMigration;
  37. LocalFileUpdateService._privateConstructor() {
  38. _logger = Logger((LocalFileUpdateService).toString());
  39. _fileUpdationDB = FileUpdationDB.instance;
  40. }
  41. void init(SharedPreferences preferences) {
  42. _prefs = preferences;
  43. }
  44. static LocalFileUpdateService instance =
  45. LocalFileUpdateService._privateConstructor();
  46. bool isBadCreationMigrationCompleted() {
  47. return (_prefs.getBool(isBadCreationTimeMigrationComplete) ?? false);
  48. }
  49. Future<void> markUpdatedFilesForReUpload() async {
  50. if (_existingMigration != null) {
  51. _logger.info("migration is already in progress, skipping");
  52. return _existingMigration!.future;
  53. }
  54. _existingMigration = Completer<void>();
  55. try {
  56. await _markFilesWhichAreActuallyUpdated();
  57. if (Platform.isAndroid) {
  58. await _migrationForFixingBadCreationTime();
  59. await _migrationFilesWithMissingLocationV2();
  60. await _migrationFilesWithBadLocationCord();
  61. }
  62. } catch (e, s) {
  63. _logger.severe('failed to perform migration', e, s);
  64. } finally {
  65. _existingMigration?.complete();
  66. _existingMigration = null;
  67. }
  68. }
  69. // This method analyses all of local files for which the file
  70. // modification/update time was changed. It checks if the existing fileHash
  71. // is different from the hash of uploaded file. If fileHash are different,
  72. // then it marks the file for file update.
  73. Future<void> _markFilesWhichAreActuallyUpdated() async {
  74. final sTime = DateTime.now().microsecondsSinceEpoch;
  75. // singleRunLimit indicates number of files to check during single
  76. // invocation of this method. The limit act as a crude way to limit the
  77. // resource consumed by the method
  78. const int singleRunLimit = 10;
  79. final localIDsToProcess =
  80. await _fileUpdationDB.getLocalIDsForPotentialReUpload(
  81. singleRunLimit,
  82. FileUpdationDB.modificationTimeUpdated,
  83. );
  84. if (localIDsToProcess.isNotEmpty) {
  85. await _checkAndMarkFilesWithDifferentHashForFileUpdate(
  86. localIDsToProcess,
  87. );
  88. final eTime = DateTime.now().microsecondsSinceEpoch;
  89. final d = Duration(microseconds: eTime - sTime);
  90. _logger.info(
  91. 'Performed hashCheck for ${localIDsToProcess.length} updated files '
  92. 'completed in ${d.inSeconds.toString()} secs',
  93. );
  94. }
  95. }
  96. Future<void> _checkAndMarkFilesWithDifferentHashForFileUpdate(
  97. List<String> localIDsToProcess,
  98. ) async {
  99. _logger.info("files to process ${localIDsToProcess.length} for reupload");
  100. final List<ente.File> localFiles =
  101. await FilesDB.instance.getLocalFiles(localIDsToProcess);
  102. final Set<String> processedIDs = {};
  103. for (ente.File file in localFiles) {
  104. if (processedIDs.contains(file.localID)) {
  105. continue;
  106. }
  107. MediaUploadData uploadData;
  108. try {
  109. uploadData = await getUploadData(file);
  110. if (uploadData.hashData != null &&
  111. file.hash != null &&
  112. (file.hash == uploadData.hashData!.fileHash ||
  113. file.hash == uploadData.hashData!.zipHash)) {
  114. _logger.info("Skip file update as hash matched ${file.tag}");
  115. } else {
  116. _logger.info(
  117. "Marking for file update as hash did not match ${file.tag}",
  118. );
  119. await clearCache(file);
  120. await FilesDB.instance.updateUploadedFile(
  121. file.localID!,
  122. file.title,
  123. file.location,
  124. file.creationTime!,
  125. file.modificationTime!,
  126. null,
  127. );
  128. }
  129. processedIDs.add(file.localID!);
  130. } on InvalidFileError {
  131. // if we fail to get the file, we can ignore the update
  132. processedIDs.add(file.localID!);
  133. } catch (e) {
  134. _logger.severe("Failed to get file uploadData", e);
  135. } finally {}
  136. }
  137. debugPrint("Deleting files ${processedIDs.length}");
  138. await _fileUpdationDB.deleteByLocalIDs(
  139. processedIDs.toList(),
  140. FileUpdationDB.modificationTimeUpdated,
  141. );
  142. }
  143. Future<MediaUploadData> getUploadData(ente.File file) async {
  144. final mediaUploadData = await getUploadDataFromEnteFile(file);
  145. // delete the file from app's internal cache if it was copied to app
  146. // for upload. Shared Media should only be cleared when the upload
  147. // succeeds.
  148. if (Platform.isIOS && mediaUploadData.sourceFile != null) {
  149. await mediaUploadData.sourceFile?.delete();
  150. }
  151. return mediaUploadData;
  152. }
  153. Future<void> _migrationForFixingBadCreationTime() async {
  154. if (_prefs.containsKey(isBadCreationTimeMigrationComplete)) {
  155. return;
  156. }
  157. await _importFilesWithBadCreationTime();
  158. const int singleRunLimit = 100;
  159. try {
  160. final generatedIDs =
  161. await _fileUpdationDB.getLocalIDsForPotentialReUpload(
  162. singleRunLimit,
  163. FileUpdationDB.badCreationTime,
  164. );
  165. if (generatedIDs.isNotEmpty) {
  166. final List<int> genIdIntList = [];
  167. for (String genIdString in generatedIDs) {
  168. final int? genIdInt = int.tryParse(genIdString);
  169. if (genIdInt != null) {
  170. genIdIntList.add(genIdInt);
  171. }
  172. }
  173. final filesWithBadTime =
  174. (await FilesDB.instance.getFilesFromGeneratedIDs(genIdIntList))
  175. .values
  176. .toList();
  177. filesWithBadTime.removeWhere(
  178. (e) => e.isUploaded && e.pubMagicMetadata?.editedTime != null,
  179. );
  180. await FilesService.instance
  181. .bulkEditTime(filesWithBadTime, EditTimeSource.fileName);
  182. } else {
  183. // everything is done
  184. await _prefs.setBool(isBadCreationTimeMigrationComplete, true);
  185. }
  186. await _fileUpdationDB.deleteByLocalIDs(
  187. generatedIDs,
  188. FileUpdationDB.badCreationTime,
  189. );
  190. } catch (e) {
  191. _logger.severe("Failed to fix bad creationTime", e);
  192. }
  193. }
  194. Future<void> _importFilesWithBadCreationTime() async {
  195. if (_prefs.containsKey(isBadCreationTimeImportDone)) {
  196. return;
  197. }
  198. _logger.info('_importFilesWithBadCreationTime');
  199. final EnteWatch watch = EnteWatch("_importFilesWithBadCreationTime");
  200. final int ownerID = Configuration.instance.getUserID()!;
  201. final filesGeneratedID = await FilesDB.instance
  202. .getGeneratedIDForFilesOlderThan(jan011981Time, ownerID);
  203. await _fileUpdationDB.insertMultiple(
  204. filesGeneratedID,
  205. FileUpdationDB.badCreationTime,
  206. );
  207. watch.log("imported ${filesGeneratedID.length} files");
  208. _prefs.setBool(isBadCreationTimeImportDone, true);
  209. }
  210. Future<void> _migrationFilesWithMissingLocationV2() async {
  211. if (_prefs.containsKey(isMissingLocationV2MigrationDone)) {
  212. return;
  213. }
  214. await _importForMissingLocationV2();
  215. const int singleRunLimit = 10;
  216. final List<String> processedIDs = [];
  217. try {
  218. final localIDs = await _fileUpdationDB.getLocalIDsForPotentialReUpload(
  219. singleRunLimit,
  220. FileUpdationDB.missingLocationV2,
  221. );
  222. if (localIDs.isEmpty) {
  223. // everything is done
  224. await _prefs.setBool(isMissingLocationV2MigrationDone, true);
  225. return;
  226. }
  227. final List<ente.File> enteFiles = await FilesDB.instance
  228. .getFilesForLocalIDs(localIDs, Configuration.instance.getUserID()!);
  229. // fine localIDs which are not present in enteFiles
  230. final List<String> missingLocalIDs = [];
  231. for (String localID in localIDs) {
  232. if (enteFiles.firstWhereOrNull((e) => e.localID == localID) == null) {
  233. missingLocalIDs.add(localID);
  234. }
  235. }
  236. processedIDs.addAll(missingLocalIDs);
  237. final List<ente.File> remoteFilesToUpdate = [];
  238. final Map<int, Map<String, double>> fileIDToUpdateMetadata = {};
  239. for (ente.File file in enteFiles) {
  240. final Location? location = await tryLocationFromExif(file);
  241. if (location != null && Location.isValidLocation(location)) {
  242. remoteFilesToUpdate.add(file);
  243. fileIDToUpdateMetadata[file.uploadedFileID!] = {
  244. pubMagicKeyLat: location.latitude!,
  245. pubMagicKeyLong: location.longitude!
  246. };
  247. } else if (file.localID != null) {
  248. processedIDs.add(file.localID!);
  249. }
  250. }
  251. if (remoteFilesToUpdate.isNotEmpty) {
  252. await FileMagicService.instance.updatePublicMagicMetadata(
  253. remoteFilesToUpdate,
  254. null,
  255. metadataUpdateMap: fileIDToUpdateMetadata,
  256. );
  257. for (ente.File file in remoteFilesToUpdate) {
  258. if (file.localID != null) {
  259. processedIDs.add(file.localID!);
  260. }
  261. }
  262. }
  263. } catch (e) {
  264. _logger.severe("Failed to fix bad creationTime", e);
  265. } finally {
  266. await _fileUpdationDB.deleteByLocalIDs(
  267. processedIDs,
  268. FileUpdationDB.missingLocationV2,
  269. );
  270. }
  271. }
  272. Future<void> _importForMissingLocationV2() async {
  273. if (_prefs.containsKey(isMissingLocationV2ImportDone)) {
  274. return;
  275. }
  276. _logger.info('_importForMissingLocationV2');
  277. final EnteWatch watch = EnteWatch("_importForMissingLocationV2");
  278. final int ownerID = Configuration.instance.getUserID()!;
  279. final List<String> localIDs =
  280. await FilesDB.instance.getLocalIDsForFilesWithoutLocation(ownerID);
  281. await _fileUpdationDB.insertMultiple(
  282. localIDs,
  283. FileUpdationDB.missingLocationV2,
  284. );
  285. watch.log("imported ${localIDs.length} files");
  286. await _prefs.setBool(isMissingLocationV2ImportDone, true);
  287. }
  288. Future<void> _migrationFilesWithBadLocationCord() async {
  289. if (_prefs.containsKey(isBadLocationCordMigrationDone)) {
  290. return;
  291. }
  292. await _importForBadLocationCord();
  293. const int singleRunLimit = 10;
  294. final List<String> processedIDs = [];
  295. try {
  296. final localIDs = await _fileUpdationDB.getLocalIDsForPotentialReUpload(
  297. singleRunLimit,
  298. FileUpdationDB.badLocationCord,
  299. );
  300. if (localIDs.isEmpty) {
  301. // everything is done
  302. await _prefs.setBool(isBadLocationCordMigrationDone, true);
  303. return;
  304. }
  305. final List<ente.File> enteFiles = await FilesDB.instance
  306. .getFilesForLocalIDs(localIDs, Configuration.instance.getUserID()!);
  307. // fine localIDs which are not present in enteFiles
  308. final List<String> missingLocalIDs = [];
  309. for (String localID in localIDs) {
  310. if (enteFiles.firstWhereOrNull((e) => e.localID == localID) == null) {
  311. missingLocalIDs.add(localID);
  312. }
  313. }
  314. processedIDs.addAll(missingLocalIDs);
  315. final List<ente.File> remoteFilesToUpdate = [];
  316. final Map<int, Map<String, double>> fileIDToUpdateMetadata = {};
  317. for (ente.File file in enteFiles) {
  318. final Location? location = await tryLocationFromExif(file);
  319. if (location != null &&
  320. (location.latitude ?? 0) != 0.0 &&
  321. (location.longitude ?? 0) != 0.0) {
  322. // check if the location is already correct
  323. if (file.location != null &&
  324. file.location?.longitude == location.latitude &&
  325. file.location?.longitude == location.longitude) {
  326. processedIDs.add(file.localID!);
  327. } else {
  328. remoteFilesToUpdate.add(file);
  329. fileIDToUpdateMetadata[file.uploadedFileID!] = {
  330. pubMagicKeyLat: location.latitude!,
  331. pubMagicKeyLong: location.longitude!
  332. };
  333. }
  334. } else if (file.localID != null) {
  335. processedIDs.add(file.localID!);
  336. }
  337. }
  338. if (remoteFilesToUpdate.isNotEmpty) {
  339. await FileMagicService.instance.updatePublicMagicMetadata(
  340. remoteFilesToUpdate,
  341. null,
  342. metadataUpdateMap: fileIDToUpdateMetadata,
  343. );
  344. for (ente.File file in remoteFilesToUpdate) {
  345. if (file.localID != null) {
  346. processedIDs.add(file.localID!);
  347. }
  348. }
  349. }
  350. } catch (e) {
  351. _logger.severe("Failed to fix bad location cord", e);
  352. } finally {
  353. await _fileUpdationDB.deleteByLocalIDs(
  354. processedIDs,
  355. FileUpdationDB.badLocationCord,
  356. );
  357. }
  358. }
  359. Future<void> _importForBadLocationCord() async {
  360. if (_prefs.containsKey(isBadLocationCordImportDone)) {
  361. return;
  362. }
  363. _logger.info('_importForBadLocationCord');
  364. final EnteWatch watch = EnteWatch("_importForBadLocationCord");
  365. final int ownerID = Configuration.instance.getUserID()!;
  366. final List<String> localIDs = await FilesDB.instance
  367. .getFilesWithLocationUploadedBtw20AprTo15May2023(ownerID);
  368. await _fileUpdationDB.insertMultiple(
  369. localIDs,
  370. FileUpdationDB.badLocationCord,
  371. );
  372. watch.log("imported ${localIDs.length} files");
  373. await _prefs.setBool(isBadLocationCordImportDone, true);
  374. }
  375. }