file_util.dart 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import 'dart:async';
  2. import 'dart:io' as io;
  3. import 'dart:typed_data';
  4. import 'package:dio/dio.dart';
  5. import 'package:flutter_cache_manager/flutter_cache_manager.dart';
  6. import 'package:flutter_image_compress/flutter_image_compress.dart';
  7. import 'package:flutter_sodium/flutter_sodium.dart';
  8. import 'package:logging/logging.dart';
  9. import 'package:path/path.dart';
  10. import 'package:photos/core/cache/image_cache.dart';
  11. import 'package:photos/core/cache/video_cache_manager.dart';
  12. import 'package:photos/core/configuration.dart';
  13. import 'package:photos/core/constants.dart';
  14. import 'package:photos/core/network.dart';
  15. import 'package:photos/models/file.dart' as ente;
  16. import 'package:photos/models/file_type.dart';
  17. import 'package:photos/services/collections_service.dart';
  18. import 'package:photos/utils/thumbnail_util.dart';
  19. import 'crypto_util.dart';
  20. import 'file_uploader_util.dart';
  21. final _logger = Logger("FileUtil");
  22. void preloadFile(ente.File file) {
  23. if (file.fileType == FileType.video) {
  24. return;
  25. }
  26. getFile(file);
  27. }
  28. Future<io.File> getFile(ente.File file) async {
  29. if (file.isRemoteFile()) {
  30. return getFileFromServer(file);
  31. } else {
  32. final cachedFile = FileLruCache.get(file);
  33. if (cachedFile == null) {
  34. final diskFile = await _getLocalDiskFile(file);
  35. FileLruCache.put(file, diskFile);
  36. return diskFile;
  37. }
  38. return cachedFile;
  39. }
  40. }
  41. Future<bool> doesLocalFileExist(ente.File file) async {
  42. return await _getLocalDiskFile(file) != null;
  43. }
  44. Future<io.File> _getLocalDiskFile(ente.File file) async {
  45. if (file.isSharedMediaToAppSandbox()) {
  46. var localFile = io.File(getSharedMediaFilePath(file));
  47. return localFile.exists().then((exist) {
  48. return exist ? localFile : null;
  49. });
  50. } else {
  51. return file.getAsset().then((asset) async {
  52. if (asset == null || !(await asset.exists)) {
  53. return null;
  54. }
  55. return asset.file;
  56. });
  57. }
  58. }
  59. String getSharedMediaFilePath(ente.File file) {
  60. return Configuration.instance.getSharedMediaCacheDirectory()
  61. + "/" + file.localID.replaceAll(kSharedMediaIdentifier, '');
  62. }
  63. void preloadThumbnail(ente.File file) {
  64. if (file.isRemoteFile()) {
  65. getThumbnailFromServer(file);
  66. } else {
  67. getThumbnailFromLocal(file);
  68. }
  69. }
  70. final Map<int, Future<io.File>> fileDownloadsInProgress =
  71. Map<int, Future<io.File>>();
  72. Future<io.File> getFileFromServer(ente.File file,
  73. {ProgressCallback progressCallback}) async {
  74. final cacheManager = file.fileType == FileType.video
  75. ? VideoCacheManager.instance
  76. : DefaultCacheManager();
  77. return cacheManager.getFileFromCache(file.getDownloadUrl()).then((info) {
  78. if (info == null) {
  79. if (!fileDownloadsInProgress.containsKey(file.uploadedFileID)) {
  80. fileDownloadsInProgress[file.uploadedFileID] = _downloadAndDecrypt(
  81. file,
  82. cacheManager,
  83. progressCallback: progressCallback,
  84. );
  85. }
  86. return fileDownloadsInProgress[file.uploadedFileID];
  87. } else {
  88. return info.file;
  89. }
  90. });
  91. }
  92. Future<io.File> _downloadAndDecrypt(
  93. ente.File file, BaseCacheManager cacheManager,
  94. {ProgressCallback progressCallback}) async {
  95. _logger.info("Downloading file " + file.uploadedFileID.toString());
  96. final encryptedFilePath = Configuration.instance.getTempDirectory() +
  97. file.generatedID.toString() +
  98. ".encrypted";
  99. final decryptedFilePath = Configuration.instance.getTempDirectory() +
  100. file.generatedID.toString() +
  101. ".decrypted";
  102. final encryptedFile = io.File(encryptedFilePath);
  103. final decryptedFile = io.File(decryptedFilePath);
  104. final startTime = DateTime.now().millisecondsSinceEpoch;
  105. return Network.instance
  106. .getDio()
  107. .download(
  108. file.getDownloadUrl(),
  109. encryptedFilePath,
  110. options: Options(
  111. headers: {"X-Auth-Token": Configuration.instance.getToken()},
  112. ),
  113. onReceiveProgress: progressCallback,
  114. )
  115. .then((response) async {
  116. if (response.statusCode != 200) {
  117. _logger.warning("Could not download file: ", response.toString());
  118. return null;
  119. } else if (!encryptedFile.existsSync()) {
  120. _logger.warning("File was not downloaded correctly.");
  121. return null;
  122. }
  123. _logger.info("File downloaded: " + file.uploadedFileID.toString());
  124. _logger.info("Download speed: " +
  125. (io.File(encryptedFilePath).lengthSync() /
  126. (DateTime.now().millisecondsSinceEpoch - startTime))
  127. .toString() +
  128. "kBps");
  129. await CryptoUtil.decryptFile(encryptedFilePath, decryptedFilePath,
  130. Sodium.base642bin(file.fileDecryptionHeader), decryptFileKey(file));
  131. _logger.info("File decrypted: " + file.uploadedFileID.toString());
  132. encryptedFile.deleteSync();
  133. var fileExtension = "unknown";
  134. try {
  135. fileExtension = extension(file.title).substring(1).toLowerCase();
  136. } catch (e) {
  137. _logger.severe("Could not capture file extension");
  138. }
  139. var outputFile = decryptedFile;
  140. if ((fileExtension == "unknown" && file.fileType == FileType.image) ||
  141. (io.Platform.isAndroid && fileExtension == "heic")) {
  142. outputFile = await FlutterImageCompress.compressAndGetFile(
  143. decryptedFilePath,
  144. decryptedFilePath + ".jpg",
  145. keepExif: true,
  146. );
  147. decryptedFile.deleteSync();
  148. }
  149. final cachedFile = await cacheManager.putFile(
  150. file.getDownloadUrl(),
  151. outputFile.readAsBytesSync(),
  152. eTag: file.getDownloadUrl(),
  153. maxAge: Duration(days: 365),
  154. fileExtension: fileExtension,
  155. );
  156. outputFile.deleteSync();
  157. fileDownloadsInProgress.remove(file.uploadedFileID);
  158. return cachedFile;
  159. }).catchError((e) {
  160. fileDownloadsInProgress.remove(file.uploadedFileID);
  161. });
  162. }
  163. Uint8List decryptFileKey(ente.File file) {
  164. final encryptedKey = Sodium.base642bin(file.encryptedKey);
  165. final nonce = Sodium.base642bin(file.keyDecryptionNonce);
  166. final collectionKey =
  167. CollectionsService.instance.getCollectionKey(file.collectionID);
  168. return CryptoUtil.decryptSync(encryptedKey, collectionKey, nonce);
  169. }
  170. Future<Uint8List> compressThumbnail(Uint8List thumbnail) {
  171. return FlutterImageCompress.compressWithList(
  172. thumbnail,
  173. minHeight: kCompressedThumbnailResolution,
  174. minWidth: kCompressedThumbnailResolution,
  175. quality: 25,
  176. );
  177. }
  178. void clearCache(ente.File file) {
  179. if (file.fileType == FileType.video) {
  180. VideoCacheManager.instance.removeFile(file.getDownloadUrl());
  181. } else {
  182. DefaultCacheManager().removeFile(file.getDownloadUrl());
  183. }
  184. final cachedThumbnail = io.File(
  185. Configuration.instance.getThumbnailCacheDirectory() +
  186. "/" +
  187. file.uploadedFileID.toString());
  188. if (cachedThumbnail.existsSync()) {
  189. cachedThumbnail.deleteSync();
  190. }
  191. }