sync_service.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:connectivity/connectivity.dart';
  4. import 'package:flutter/foundation.dart';
  5. import 'package:flutter_cache_manager/flutter_cache_manager.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:photos/core/cache/thumbnail_cache_manager.dart';
  8. import 'package:photos/core/cache/video_cache_manager.dart';
  9. import 'package:photos/core/event_bus.dart';
  10. import 'package:photos/core/network.dart';
  11. import 'package:photos/db/files_db.dart';
  12. import 'package:photos/events/collection_updated_event.dart';
  13. import 'package:photos/events/sync_status_update_event.dart';
  14. import 'package:photos/events/user_authenticated_event.dart';
  15. import 'package:photos/models/file_type.dart';
  16. import 'package:photos/services/billing_service.dart';
  17. import 'package:photos/services/collections_service.dart';
  18. import 'package:photos/utils/date_time_util.dart';
  19. import 'package:photos/utils/file_downloader.dart';
  20. import 'package:photos/repositories/file_repository.dart';
  21. import 'package:photo_manager/photo_manager.dart';
  22. import 'package:photos/utils/file_sync_util.dart';
  23. import 'package:photos/utils/file_uploader.dart';
  24. import 'package:shared_preferences/shared_preferences.dart';
  25. import 'package:dio/dio.dart';
  26. import 'package:photos/models/file.dart';
  27. import 'package:photos/core/configuration.dart';
  28. class SyncService {
  29. final _logger = Logger("SyncService");
  30. final _dio = Network.instance.getDio();
  31. final _db = FilesDB.instance;
  32. final _uploader = FileUploader.instance;
  33. final _collectionsService = CollectionsService.instance;
  34. final _downloader = DiffFetcher();
  35. bool _isSyncInProgress = false;
  36. bool _syncStopRequested = false;
  37. Future<void> _existingSync;
  38. SharedPreferences _prefs;
  39. SyncStatusUpdate _lastSyncStatusEvent;
  40. static final _dbUpdationTimeKey = "db_updation_time";
  41. static final _diffLimit = 200;
  42. SyncService._privateConstructor() {
  43. Bus.instance.on<UserAuthenticatedEvent>().listen((event) {
  44. sync();
  45. });
  46. Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
  47. _logger.info("Connectivity change detected " + result.toString());
  48. if (Configuration.instance.hasConfiguredAccount() &&
  49. BillingService.instance.getSubscription() != null) {
  50. sync(isAppInBackground: true);
  51. }
  52. });
  53. Bus.instance.on<SyncStatusUpdate>().listen((event) {
  54. _lastSyncStatusEvent = event;
  55. });
  56. }
  57. static final SyncService instance = SyncService._privateConstructor();
  58. Future<void> init() async {
  59. _prefs = await SharedPreferences.getInstance();
  60. if (Platform.isIOS) {
  61. _logger.info("Clearing file cache");
  62. await PhotoManager.clearFileCache();
  63. _logger.info("Cleared file cache");
  64. }
  65. }
  66. Future<void> sync({bool isAppInBackground = false}) async {
  67. _syncStopRequested = false;
  68. if (_isSyncInProgress) {
  69. _logger.warning("Sync already in progress, skipping.");
  70. return _existingSync;
  71. }
  72. _isSyncInProgress = true;
  73. _existingSync = Future<void>(() async {
  74. _logger.info("Syncing...");
  75. try {
  76. await _doSync(isAppInBackground: isAppInBackground);
  77. if (_lastSyncStatusEvent != null) {
  78. Bus.instance.fire(SyncStatusUpdate(SyncStatus.completed));
  79. }
  80. } on WiFiUnavailableError {
  81. _logger.warning("Not uploading over mobile data");
  82. Bus.instance.fire(
  83. SyncStatusUpdate(SyncStatus.paused, reason: "Waiting for WiFi..."));
  84. } on SyncStopRequestedError {
  85. _syncStopRequested = false;
  86. Bus.instance
  87. .fire(SyncStatusUpdate(SyncStatus.completed, wasStopped: true));
  88. } catch (e, s) {
  89. _logger.severe(e, s);
  90. Bus.instance.fire(SyncStatusUpdate(SyncStatus.error));
  91. } finally {
  92. _isSyncInProgress = false;
  93. }
  94. });
  95. return _existingSync;
  96. }
  97. void stopSync() {
  98. _logger.info("Sync stop requested");
  99. _syncStopRequested = true;
  100. }
  101. bool shouldStopSync() {
  102. return _syncStopRequested;
  103. }
  104. bool hasScannedDisk() {
  105. return _prefs.containsKey(_dbUpdationTimeKey);
  106. }
  107. bool isSyncInProgress() {
  108. return _isSyncInProgress;
  109. }
  110. SyncStatusUpdate getLastSyncStatusEvent() {
  111. return _lastSyncStatusEvent;
  112. }
  113. Future<void> _doSync({bool isAppInBackground = false}) async {
  114. final existingLocalFileIDs = await _db.getExistingLocalFileIDs();
  115. final syncStartTime = DateTime.now().microsecondsSinceEpoch;
  116. if (isAppInBackground) {
  117. await PhotoManager.setIgnorePermissionCheck(true);
  118. } else {
  119. final result = await PhotoManager.requestPermission();
  120. if (!result) {
  121. _logger.severe("Did not get permission");
  122. await _prefs.setInt(_dbUpdationTimeKey, syncStartTime);
  123. await FileRepository.instance.reloadFiles();
  124. return await syncWithRemote();
  125. }
  126. }
  127. final lastDBUpdationTime = _prefs.getInt(_dbUpdationTimeKey);
  128. if (lastDBUpdationTime != null && lastDBUpdationTime != 0) {
  129. await _loadAndStorePhotos(
  130. lastDBUpdationTime, syncStartTime, existingLocalFileIDs);
  131. } else {
  132. // Load from 0 - 01.01.2010
  133. var startTime = 0;
  134. var toYear = 2010;
  135. var toTime = DateTime(toYear).microsecondsSinceEpoch;
  136. while (toTime < syncStartTime) {
  137. await _loadAndStorePhotos(startTime, toTime, existingLocalFileIDs);
  138. startTime = toTime;
  139. toYear++;
  140. toTime = DateTime(toYear).microsecondsSinceEpoch;
  141. }
  142. await _loadAndStorePhotos(startTime, syncStartTime, existingLocalFileIDs);
  143. }
  144. await FileRepository.instance.reloadFiles();
  145. // await syncWithRemote();
  146. }
  147. Future<void> _loadAndStorePhotos(
  148. int fromTime, int toTime, Set<String> existingLocalFileIDs) async {
  149. _logger.info("Loading photos from " +
  150. getMonthAndYear(DateTime.fromMicrosecondsSinceEpoch(fromTime)) +
  151. " to " +
  152. getMonthAndYear(DateTime.fromMicrosecondsSinceEpoch(toTime)));
  153. final files = await getDeviceFiles(fromTime, toTime);
  154. if (files.isNotEmpty) {
  155. Bus.instance.fire(SyncStatusUpdate(SyncStatus.applying_local_diff));
  156. _logger.info("Fetched " + files.length.toString() + " files.");
  157. final updatedFiles =
  158. files.where((file) => existingLocalFileIDs.contains(file.localID));
  159. _logger.info(updatedFiles.length.toString() + " files were updated.");
  160. for (final file in updatedFiles) {
  161. await _db.updateUploadedFile(
  162. file.localID,
  163. file.title,
  164. file.location,
  165. file.creationTime,
  166. file.modificationTime,
  167. null,
  168. );
  169. }
  170. files.removeWhere((file) => existingLocalFileIDs.contains(file.localID));
  171. await _db.insertMultiple(files);
  172. _logger.info("Inserted " + files.length.toString() + " files.");
  173. await FileRepository.instance.reloadFiles();
  174. }
  175. await _prefs.setInt(_dbUpdationTimeKey, toTime);
  176. }
  177. Future<void> syncWithRemote({bool silently = false}) async {
  178. if (!Configuration.instance.hasConfiguredAccount()) {
  179. return Future.error("Account not configured yet");
  180. }
  181. final updatedCollections = await _collectionsService.sync();
  182. if (updatedCollections.isNotEmpty && !silently) {
  183. Bus.instance.fire(SyncStatusUpdate(SyncStatus.applying_remote_diff));
  184. }
  185. for (final collection in updatedCollections) {
  186. await _fetchEncryptedFilesDiff(collection.id);
  187. }
  188. await deleteFilesOnServer();
  189. bool hasUploadedFiles = await _uploadDiff();
  190. if (hasUploadedFiles) {
  191. syncWithRemote(silently: true);
  192. }
  193. }
  194. Future<void> _fetchEncryptedFilesDiff(int collectionID) async {
  195. final diff = await _downloader.getEncryptedFilesDiff(
  196. collectionID,
  197. _collectionsService.getCollectionSyncTime(collectionID),
  198. _diffLimit,
  199. );
  200. if (diff.updatedFiles.isNotEmpty) {
  201. await _storeDiff(diff.updatedFiles, collectionID);
  202. _logger.info("Updated " +
  203. diff.updatedFiles.length.toString() +
  204. " files in collection " +
  205. collectionID.toString());
  206. FileRepository.instance.reloadFiles();
  207. Bus.instance.fire(CollectionUpdatedEvent(collectionID: collectionID));
  208. if (diff.fetchCount == _diffLimit) {
  209. return await _fetchEncryptedFilesDiff(collectionID);
  210. }
  211. }
  212. }
  213. Future<bool> _uploadDiff() async {
  214. final foldersToBackUp = Configuration.instance.getPathsToBackUp();
  215. final filesToBeUploaded =
  216. await _db.getFilesToBeUploadedWithinFolders(foldersToBackUp);
  217. if (kDebugMode) {
  218. filesToBeUploaded
  219. .removeWhere((element) => element.fileType == FileType.video);
  220. }
  221. _logger.info(
  222. filesToBeUploaded.length.toString() + " new files to be uploaded.");
  223. final updatedFileIDs = await _db.getUploadedFileIDsToBeUpdated();
  224. _logger.info(updatedFileIDs.length.toString() + " files updated.");
  225. int uploadCounter = 0;
  226. final totalUploads = filesToBeUploaded.length + updatedFileIDs.length;
  227. if (totalUploads > 0) {
  228. Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparing_for_upload));
  229. }
  230. final futures = List<Future>();
  231. for (final uploadedFileID in updatedFileIDs) {
  232. final file = await _db.getUploadedFileInAnyCollection(uploadedFileID);
  233. final future = _uploader.upload(file, file.collectionID).then((value) {
  234. uploadCounter++;
  235. Bus.instance
  236. .fire(CollectionUpdatedEvent(collectionID: file.collectionID));
  237. Bus.instance.fire(SyncStatusUpdate(SyncStatus.in_progress,
  238. completed: uploadCounter, total: totalUploads));
  239. });
  240. futures.add(future);
  241. }
  242. for (final file in filesToBeUploaded) {
  243. final collectionID = (await CollectionsService.instance
  244. .getOrCreateForPath(file.deviceFolder))
  245. .id;
  246. final future = _uploader.upload(file, collectionID).then((value) {
  247. uploadCounter++;
  248. Bus.instance
  249. .fire(CollectionUpdatedEvent(collectionID: file.collectionID));
  250. Bus.instance.fire(SyncStatusUpdate(SyncStatus.in_progress,
  251. completed: uploadCounter, total: totalUploads));
  252. });
  253. futures.add(future);
  254. }
  255. try {
  256. await Future.wait(futures);
  257. } on InvalidFileError {
  258. // Do nothing
  259. } on WiFiUnavailableError {
  260. throw WiFiUnavailableError();
  261. } on SyncStopRequestedError {
  262. throw SyncStopRequestedError();
  263. } catch (e, s) {
  264. _isSyncInProgress = false;
  265. Bus.instance.fire(SyncStatusUpdate(SyncStatus.error));
  266. _logger.severe("Error in syncing files", e, s);
  267. }
  268. return uploadCounter > 0;
  269. }
  270. Future _storeDiff(List<File> diff, int collectionID) async {
  271. for (File file in diff) {
  272. final existingFiles = await _db.getMatchingFiles(
  273. file.title, file.deviceFolder, file.creationTime);
  274. if (existingFiles == null) {
  275. // File uploaded from a different device
  276. file.localID = null;
  277. await _db.insert(file);
  278. } else {
  279. // File exists on device
  280. file.localID = existingFiles[0]
  281. .localID; // File should ideally have the same localID
  282. bool wasUploadedOnAPreviousInstallation =
  283. existingFiles.length == 1 && existingFiles[0].collectionID == null;
  284. if (wasUploadedOnAPreviousInstallation) {
  285. file.generatedID = existingFiles[0].generatedID;
  286. if (file.modificationTime != existingFiles[0].modificationTime) {
  287. // File was updated since the app was uninstalled
  288. _logger.info("Updated since last installation: " +
  289. file.uploadedFileID.toString());
  290. file.updationTime = null;
  291. }
  292. await _db.update(file);
  293. } else {
  294. bool foundMatchingCollection = false;
  295. for (final existingFile in existingFiles) {
  296. if (file.collectionID == existingFile.collectionID &&
  297. file.uploadedFileID == existingFile.uploadedFileID) {
  298. foundMatchingCollection = true;
  299. file.generatedID = existingFile.generatedID;
  300. await _db.update(file);
  301. if (file.fileType == FileType.video) {
  302. VideoCacheManager().removeFile(file.getDownloadUrl());
  303. } else {
  304. DefaultCacheManager().removeFile(file.getDownloadUrl());
  305. }
  306. ThumbnailCacheManager().removeFile(file.getDownloadUrl());
  307. break;
  308. }
  309. }
  310. if (!foundMatchingCollection) {
  311. // Added to a new collection
  312. await _db.insert(file);
  313. }
  314. }
  315. }
  316. await _collectionsService.setCollectionSyncTime(
  317. collectionID, file.updationTime);
  318. }
  319. }
  320. Future<void> deleteFilesOnServer() async {
  321. return _db.getDeletedFileIDs().then((ids) async {
  322. for (int id in ids) {
  323. await _deleteFileOnServer(id);
  324. await _db.delete(id);
  325. }
  326. });
  327. }
  328. Future<void> _deleteFileOnServer(int fileID) async {
  329. return _dio
  330. .delete(
  331. Configuration.instance.getHttpEndpoint() +
  332. "/files/" +
  333. fileID.toString(),
  334. options: Options(
  335. headers: {"X-Auth-Token": Configuration.instance.getToken()}),
  336. )
  337. .catchError((e) => _logger.severe(e));
  338. }
  339. }