file_util.dart 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. import 'dart:io' as io;
  2. import 'dart:io';
  3. import 'dart:typed_data';
  4. import 'package:flutter_sodium/flutter_sodium.dart';
  5. import 'package:logging/logging.dart';
  6. import 'package:path/path.dart';
  7. import 'package:dio/dio.dart';
  8. import 'package:flutter_cache_manager/flutter_cache_manager.dart';
  9. import 'package:flutter_image_compress/flutter_image_compress.dart';
  10. import 'package:photo_manager/photo_manager.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/event_bus.dart';
  18. import 'package:photos/core/network.dart';
  19. import 'package:photos/db/files_db.dart';
  20. import 'package:photos/events/collection_updated_event.dart';
  21. import 'package:photos/models/file.dart';
  22. import 'package:photos/models/file_type.dart';
  23. import 'package:photos/repositories/file_repository.dart';
  24. import 'package:photos/services/collections_service.dart';
  25. import 'package:photos/services/sync_service.dart';
  26. import 'crypto_util.dart';
  27. final _logger = Logger("FileUtil");
  28. Future<void> deleteFiles(List<File> files) async {
  29. final localIDs = List<String>();
  30. bool hasUploadedFiles = false;
  31. for (final file in files) {
  32. if (file.localID != null) {
  33. localIDs.add(file.localID);
  34. }
  35. if (file.uploadedFileID != null) {
  36. hasUploadedFiles = true;
  37. await FilesDB.instance.markForDeletion(file.uploadedFileID);
  38. } else {
  39. await FilesDB.instance.deleteLocalFile(file.localID);
  40. }
  41. }
  42. await PhotoManager.editor.deleteWithIds(localIDs);
  43. await FileRepository.instance.reloadFiles();
  44. if (hasUploadedFiles) {
  45. Bus.instance.fire(CollectionUpdatedEvent());
  46. // TODO: Blocking call?
  47. SyncService.instance.deleteFilesOnServer();
  48. }
  49. }
  50. void preloadFile(File file) {
  51. if (file.fileType == FileType.video) {
  52. return;
  53. }
  54. if (file.localID == null) {
  55. // getFileFromServer(file);
  56. } else {
  57. if (FileLruCache.get(file) == null) {
  58. file.getAsset().then((asset) {
  59. asset.file.then((assetFile) {
  60. FileLruCache.put(file, assetFile);
  61. });
  62. });
  63. }
  64. }
  65. }
  66. void preloadLocalFileThumbnail(File file) {
  67. if (file.localID == null ||
  68. ThumbnailLruCache.get(file, THUMBNAIL_SMALL_SIZE) != null) {
  69. return;
  70. }
  71. file.getAsset().then((asset) {
  72. asset
  73. .thumbDataWithSize(
  74. THUMBNAIL_SMALL_SIZE,
  75. THUMBNAIL_SMALL_SIZE,
  76. quality: THUMBNAIL_SMALL_SIZE_QUALITY,
  77. )
  78. .then((data) {
  79. ThumbnailLruCache.put(file, THUMBNAIL_SMALL_SIZE, data);
  80. });
  81. });
  82. }
  83. Future<io.File> getNativeFile(File file) async {
  84. if (file.localID == null) {
  85. return getFileFromServer(file);
  86. } else {
  87. return file.getAsset().then((asset) => asset.file);
  88. }
  89. }
  90. Future<Uint8List> getBytes(File file, {int quality = 100}) async {
  91. if (file.localID == null) {
  92. return getFileFromServer(file).then((file) => file.readAsBytesSync());
  93. } else {
  94. return await getBytesFromDisk(file, quality: quality);
  95. }
  96. }
  97. Future<Uint8List> getBytesFromDisk(File file, {int quality = 100}) async {
  98. final originalBytes = (await file.getAsset()).originBytes;
  99. if (extension(file.title) == ".HEIC" || quality != 100) {
  100. return originalBytes.then((bytes) {
  101. return FlutterImageCompress.compressWithList(bytes, quality: quality)
  102. .then((converted) {
  103. return Uint8List.fromList(converted);
  104. });
  105. });
  106. } else {
  107. return originalBytes;
  108. }
  109. }
  110. final Map<int, Future<io.File>> fileDownloadsInProgress =
  111. Map<int, Future<io.File>>();
  112. final Map<int, Future<io.File>> thumbnailDownloadsInProgress =
  113. Map<int, Future<io.File>>();
  114. Future<io.File> getFileFromServer(File file,
  115. {ProgressCallback progressCallback}) async {
  116. final cacheManager = file.fileType == FileType.video
  117. ? VideoCacheManager()
  118. : DefaultCacheManager();
  119. if (!file.isEncrypted) {
  120. return cacheManager.getSingleFile(file.getDownloadUrl());
  121. } else {
  122. return cacheManager.getFileFromCache(file.getDownloadUrl()).then((info) {
  123. if (info == null) {
  124. if (!fileDownloadsInProgress.containsKey(file.uploadedFileID)) {
  125. fileDownloadsInProgress[file.uploadedFileID] = _downloadAndDecrypt(
  126. file,
  127. cacheManager,
  128. progressCallback: progressCallback,
  129. );
  130. }
  131. return fileDownloadsInProgress[file.uploadedFileID];
  132. } else {
  133. return info.file;
  134. }
  135. });
  136. }
  137. }
  138. Future<io.File> getThumbnailFromServer(File file) async {
  139. if (!file.isEncrypted) {
  140. return ThumbnailCacheManager()
  141. .getSingleFile(file.getThumbnailUrl())
  142. .then((data) {
  143. ThumbnailFileLruCache.put(file, data);
  144. return data;
  145. });
  146. } else {
  147. return ThumbnailCacheManager()
  148. .getFileFromCache(file.getThumbnailUrl())
  149. .then((info) {
  150. if (info == null) {
  151. if (!thumbnailDownloadsInProgress.containsKey(file.uploadedFileID)) {
  152. thumbnailDownloadsInProgress[file.uploadedFileID] =
  153. _downloadAndDecryptThumbnail(file).then((data) {
  154. ThumbnailFileLruCache.put(file, data);
  155. return data;
  156. });
  157. }
  158. return thumbnailDownloadsInProgress[file.uploadedFileID];
  159. } else {
  160. ThumbnailFileLruCache.put(file, info.file);
  161. return info.file;
  162. }
  163. });
  164. }
  165. }
  166. Future<io.File> _downloadAndDecrypt(File file, BaseCacheManager cacheManager,
  167. {ProgressCallback progressCallback}) async {
  168. _logger.info("Downloading file " + file.uploadedFileID.toString());
  169. final encryptedFilePath = Configuration.instance.getTempDirectory() +
  170. file.generatedID.toString() +
  171. ".encrypted";
  172. final decryptedFilePath = Configuration.instance.getTempDirectory() +
  173. file.generatedID.toString() +
  174. ".decrypted";
  175. final encryptedFile = io.File(encryptedFilePath);
  176. final decryptedFile = io.File(decryptedFilePath);
  177. final startTime = DateTime.now().millisecondsSinceEpoch;
  178. return Network.instance
  179. .getDio()
  180. .download(
  181. file.getDownloadUrl(),
  182. encryptedFilePath,
  183. onReceiveProgress: progressCallback,
  184. )
  185. .then((response) async {
  186. if (response.statusCode != 200) {
  187. _logger.warning("Could not download file: ", response.toString());
  188. return null;
  189. } else if (!encryptedFile.existsSync()) {
  190. _logger.warning("File was not downloaded correctly.");
  191. return null;
  192. }
  193. _logger.info("File downloaded: " + file.uploadedFileID.toString());
  194. _logger.info("Download speed: " +
  195. (io.File(encryptedFilePath).lengthSync() /
  196. (DateTime.now().millisecondsSinceEpoch - startTime))
  197. .toString() +
  198. "kBps");
  199. await CryptoUtil.decryptFile(encryptedFilePath, decryptedFilePath,
  200. Sodium.base642bin(file.fileDecryptionHeader), decryptFileKey(file));
  201. _logger.info("File decrypted: " + file.uploadedFileID.toString());
  202. encryptedFile.deleteSync();
  203. var fileExtension = extension(file.title).substring(1).toLowerCase();
  204. var outputFile = decryptedFile;
  205. if (Platform.isAndroid && fileExtension == "heic") {
  206. outputFile = await FlutterImageCompress.compressAndGetFile(
  207. decryptedFilePath,
  208. decryptedFilePath + ".jpg",
  209. keepExif: true,
  210. );
  211. decryptedFile.deleteSync();
  212. }
  213. final cachedFile = await cacheManager.putFile(
  214. file.getDownloadUrl(),
  215. outputFile.readAsBytesSync(),
  216. eTag: file.getDownloadUrl(),
  217. maxAge: Duration(days: 365),
  218. fileExtension: fileExtension,
  219. );
  220. outputFile.deleteSync();
  221. fileDownloadsInProgress.remove(file.uploadedFileID);
  222. return cachedFile;
  223. }).catchError((e) {
  224. fileDownloadsInProgress.remove(file.uploadedFileID);
  225. });
  226. }
  227. Future<io.File> _downloadAndDecryptThumbnail(File file) async {
  228. _logger.info("Downloading thumbnail for " + file.uploadedFileID.toString());
  229. _logger.info("Downloads in progress " +
  230. thumbnailDownloadsInProgress.length.toString());
  231. final temporaryPath = Configuration.instance.getTempDirectory() +
  232. file.generatedID.toString() +
  233. "_thumbnail.decrypted";
  234. return Network.instance
  235. .getDio()
  236. .download(file.getThumbnailUrl(), temporaryPath)
  237. .then((_) async {
  238. final encryptedFile = io.File(temporaryPath);
  239. final thumbnailDecryptionKey = decryptFileKey(file);
  240. var data = CryptoUtil.decryptChaCha(
  241. encryptedFile.readAsBytesSync(),
  242. thumbnailDecryptionKey,
  243. Sodium.base642bin(file.thumbnailDecryptionHeader),
  244. );
  245. if (data.length > THUMBNAIL_DATA_LIMIT) {
  246. data = await FlutterImageCompress.compressWithList(
  247. data,
  248. quality: 50,
  249. minHeight: THUMBNAIL_SMALL_SIZE,
  250. minWidth: THUMBNAIL_SMALL_SIZE,
  251. );
  252. }
  253. encryptedFile.deleteSync();
  254. final cachedThumbnail = ThumbnailCacheManager().putFile(
  255. file.getThumbnailUrl(),
  256. data,
  257. eTag: file.getThumbnailUrl(),
  258. maxAge: Duration(days: 365),
  259. );
  260. thumbnailDownloadsInProgress.remove(file.uploadedFileID);
  261. return cachedThumbnail;
  262. }).catchError((e, s) {
  263. _logger.severe("Error downloading thumbnail ", e, s);
  264. thumbnailDownloadsInProgress.remove(file.uploadedFileID);
  265. });
  266. }
  267. Uint8List decryptFileKey(File file) {
  268. final encryptedKey = Sodium.base642bin(file.encryptedKey);
  269. final nonce = Sodium.base642bin(file.keyDecryptionNonce);
  270. final collectionKey =
  271. CollectionsService.instance.getCollectionKey(file.collectionID);
  272. return CryptoUtil.decryptSync(encryptedKey, collectionKey, nonce);
  273. }