file_download_util.dart 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import 'dart:io';
  2. import "package:computer/computer.dart";
  3. import 'package:dio/dio.dart';
  4. import "package:flutter/foundation.dart";
  5. import 'package:logging/logging.dart';
  6. import 'package:path/path.dart' as file_path;
  7. import "package:photo_manager/photo_manager.dart";
  8. import 'package:photos/core/configuration.dart';
  9. import "package:photos/core/event_bus.dart";
  10. import 'package:photos/core/network/network.dart';
  11. import "package:photos/db/files_db.dart";
  12. import "package:photos/events/local_photos_updated_event.dart";
  13. import 'package:photos/models/file/file.dart';
  14. import "package:photos/models/file/file_type.dart";
  15. import "package:photos/models/ignored_file.dart";
  16. import 'package:photos/services/collections_service.dart';
  17. import "package:photos/services/ignored_files_service.dart";
  18. import "package:photos/services/local_sync_service.dart";
  19. import 'package:photos/utils/crypto_util.dart';
  20. import "package:photos/utils/data_util.dart";
  21. import "package:photos/utils/fake_progress.dart";
  22. import "package:photos/utils/file_util.dart";
  23. final _logger = Logger("file_download_util");
  24. Future<File?> downloadAndDecrypt(
  25. EnteFile file, {
  26. ProgressCallback? progressCallback,
  27. }) async {
  28. final String logPrefix = 'File-${file.uploadedFileID}:';
  29. _logger
  30. .info('$logPrefix starting download ${formatBytes(file.fileSize ?? 0)}');
  31. final String tempDir = Configuration.instance.getTempDirectory();
  32. final String encryptedFilePath = "$tempDir${file.generatedID}.encrypted";
  33. final encryptedFile = File(encryptedFilePath);
  34. final startTime = DateTime.now().millisecondsSinceEpoch;
  35. try {
  36. final response = await NetworkClient.instance.getDio().download(
  37. file.downloadUrl,
  38. encryptedFilePath,
  39. options: Options(
  40. headers: {"X-Auth-Token": Configuration.instance.getToken()},
  41. ),
  42. onReceiveProgress: (a, b) {
  43. if (kDebugMode && a >= 0 && b >= 0) {
  44. _logger.fine(
  45. "$logPrefix download progress: ${formatBytes(a)} / ${formatBytes(b)}",
  46. );
  47. }
  48. progressCallback?.call(a, b);
  49. },
  50. );
  51. if (response.statusCode != 200 || !encryptedFile.existsSync()) {
  52. _logger.warning('$logPrefix download failed ${response.toString()}');
  53. return null;
  54. }
  55. final int sizeInBytes = file.fileSize ?? await encryptedFile.length();
  56. final double elapsedSeconds =
  57. (DateTime.now().millisecondsSinceEpoch - startTime) / 1000;
  58. final double speedInKBps = sizeInBytes / 1024.0 / elapsedSeconds;
  59. _logger.info(
  60. '$logPrefix download completed: ${formatBytes(sizeInBytes)}, avg speed: ${speedInKBps.toStringAsFixed(2)} KB/s',
  61. );
  62. final String decryptedFilePath = "$tempDir${file.generatedID}.decrypted";
  63. // As decryption can take time, emit fake progress for large files during
  64. // decryption
  65. final FakePeriodicProgress? fakeProgress = file.fileType == FileType.video
  66. ? FakePeriodicProgress(
  67. callback: (count) {
  68. progressCallback?.call(sizeInBytes, sizeInBytes);
  69. },
  70. duration: const Duration(milliseconds: 5000),
  71. )
  72. : null;
  73. try {
  74. // Start the periodic callback after initial 5 seconds
  75. fakeProgress?.start();
  76. await CryptoUtil.decryptFile(
  77. encryptedFilePath,
  78. decryptedFilePath,
  79. CryptoUtil.base642bin(file.fileDecryptionHeader!),
  80. getFileKey(file),
  81. );
  82. fakeProgress?.stop();
  83. _logger.info('$logPrefix decryption completed');
  84. } catch (e, s) {
  85. fakeProgress?.stop();
  86. _logger.severe("Critical: $logPrefix failed to decrypt", e, s);
  87. return null;
  88. }
  89. await encryptedFile.delete();
  90. return File(decryptedFilePath);
  91. } catch (e, s) {
  92. _logger.severe("$logPrefix failed to download or decrypt", e, s);
  93. return null;
  94. }
  95. }
  96. Uint8List getFileKey(EnteFile file) {
  97. final encryptedKey = CryptoUtil.base642bin(file.encryptedKey!);
  98. final nonce = CryptoUtil.base642bin(file.keyDecryptionNonce!);
  99. final collectionKey =
  100. CollectionsService.instance.getCollectionKey(file.collectionID!);
  101. return CryptoUtil.decryptSync(encryptedKey, collectionKey, nonce);
  102. }
  103. Future<Uint8List> getFileKeyUsingBgWorker(EnteFile file) async {
  104. final collectionKey =
  105. CollectionsService.instance.getCollectionKey(file.collectionID!);
  106. return await Computer.shared().compute(
  107. _decryptFileKey,
  108. param: <String, dynamic>{
  109. "encryptedKey": file.encryptedKey,
  110. "keyDecryptionNonce": file.keyDecryptionNonce,
  111. "collectionKey": collectionKey,
  112. },
  113. );
  114. }
  115. Future<void> downloadToGallery(EnteFile file) async {
  116. try {
  117. final FileType type = file.fileType;
  118. final bool downloadLivePhotoOnDroid =
  119. type == FileType.livePhoto && Platform.isAndroid;
  120. AssetEntity? savedAsset;
  121. final File? fileToSave = await getFile(file);
  122. //Disabling notifications for assets changing to insert the file into
  123. //files db before triggering a sync.
  124. await PhotoManager.stopChangeNotify();
  125. if (type == FileType.image) {
  126. savedAsset = await PhotoManager.editor
  127. .saveImageWithPath(fileToSave!.path, title: file.title!);
  128. } else if (type == FileType.video) {
  129. savedAsset =
  130. await PhotoManager.editor.saveVideo(fileToSave!, title: file.title!);
  131. } else if (type == FileType.livePhoto) {
  132. final File? liveVideoFile =
  133. await getFileFromServer(file, liveVideo: true);
  134. if (liveVideoFile == null) {
  135. throw AssertionError("Live video can not be null");
  136. }
  137. if (downloadLivePhotoOnDroid) {
  138. await _saveLivePhotoOnDroid(fileToSave!, liveVideoFile, file);
  139. } else {
  140. savedAsset = await PhotoManager.editor.darwin.saveLivePhoto(
  141. imageFile: fileToSave!,
  142. videoFile: liveVideoFile,
  143. title: file.title!,
  144. );
  145. }
  146. }
  147. if (savedAsset != null) {
  148. file.localID = savedAsset.id;
  149. await FilesDB.instance.insert(file);
  150. Bus.instance.fire(
  151. LocalPhotosUpdatedEvent(
  152. [file],
  153. source: "download",
  154. ),
  155. );
  156. } else if (!downloadLivePhotoOnDroid && savedAsset == null) {
  157. _logger.severe('Failed to save assert of type $type');
  158. }
  159. } catch (e) {
  160. _logger.warning("Failed to save file", e);
  161. rethrow;
  162. } finally {
  163. await PhotoManager.startChangeNotify();
  164. LocalSyncService.instance.checkAndSync().ignore();
  165. }
  166. }
  167. Future<void> _saveLivePhotoOnDroid(
  168. File image,
  169. File video,
  170. EnteFile enteFile,
  171. ) async {
  172. debugPrint("Downloading LivePhoto on Droid");
  173. AssetEntity? savedAsset = await (PhotoManager.editor
  174. .saveImageWithPath(image.path, title: enteFile.title!));
  175. if (savedAsset == null) {
  176. throw Exception("Failed to save image of live photo");
  177. }
  178. IgnoredFile ignoreVideoFile = IgnoredFile(
  179. savedAsset.id,
  180. savedAsset.title ?? '',
  181. savedAsset.relativePath ?? 'remoteDownload',
  182. "remoteDownload",
  183. );
  184. await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
  185. final videoTitle = file_path.basenameWithoutExtension(enteFile.title!) +
  186. file_path.extension(video.path);
  187. savedAsset = (await (PhotoManager.editor.saveVideo(
  188. video,
  189. title: videoTitle,
  190. )));
  191. if (savedAsset == null) {
  192. throw Exception("Failed to save video of live photo");
  193. }
  194. ignoreVideoFile = IgnoredFile(
  195. savedAsset.id,
  196. savedAsset.title ?? videoTitle,
  197. savedAsset.relativePath ?? 'remoteDownload',
  198. "remoteDownload",
  199. );
  200. await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
  201. }
  202. Uint8List _decryptFileKey(Map<String, dynamic> args) {
  203. final encryptedKey = CryptoUtil.base642bin(args["encryptedKey"]);
  204. final nonce = CryptoUtil.base642bin(args["keyDecryptionNonce"]);
  205. return CryptoUtil.decryptSync(
  206. encryptedKey,
  207. args["collectionKey"],
  208. nonce,
  209. );
  210. }