file_util.dart 6.7 KB

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