file_util.dart 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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. options: Options(
  184. headers: {"X-Auth-Token": Configuration.instance.getToken()},
  185. ),
  186. onReceiveProgress: progressCallback,
  187. )
  188. .then((response) async {
  189. if (response.statusCode != 200) {
  190. _logger.warning("Could not download file: ", response.toString());
  191. return null;
  192. } else if (!encryptedFile.existsSync()) {
  193. _logger.warning("File was not downloaded correctly.");
  194. return null;
  195. }
  196. _logger.info("File downloaded: " + file.uploadedFileID.toString());
  197. _logger.info("Download speed: " +
  198. (io.File(encryptedFilePath).lengthSync() /
  199. (DateTime.now().millisecondsSinceEpoch - startTime))
  200. .toString() +
  201. "kBps");
  202. await CryptoUtil.decryptFile(encryptedFilePath, decryptedFilePath,
  203. Sodium.base642bin(file.fileDecryptionHeader), decryptFileKey(file));
  204. _logger.info("File decrypted: " + file.uploadedFileID.toString());
  205. encryptedFile.deleteSync();
  206. var fileExtension = extension(file.title).substring(1).toLowerCase();
  207. var outputFile = decryptedFile;
  208. if (Platform.isAndroid && fileExtension == "heic") {
  209. outputFile = await FlutterImageCompress.compressAndGetFile(
  210. decryptedFilePath,
  211. decryptedFilePath + ".jpg",
  212. keepExif: true,
  213. );
  214. decryptedFile.deleteSync();
  215. }
  216. final cachedFile = await cacheManager.putFile(
  217. file.getDownloadUrl(),
  218. outputFile.readAsBytesSync(),
  219. eTag: file.getDownloadUrl(),
  220. maxAge: Duration(days: 365),
  221. fileExtension: fileExtension,
  222. );
  223. outputFile.deleteSync();
  224. fileDownloadsInProgress.remove(file.uploadedFileID);
  225. return cachedFile;
  226. }).catchError((e) {
  227. fileDownloadsInProgress.remove(file.uploadedFileID);
  228. });
  229. }
  230. Future<io.File> _downloadAndDecryptThumbnail(File file) async {
  231. final temporaryPath = Configuration.instance.getTempDirectory() +
  232. file.generatedID.toString() +
  233. "_thumbnail.decrypted";
  234. return Network.instance
  235. .getDio()
  236. .download(
  237. file.getThumbnailUrl(),
  238. temporaryPath,
  239. options: Options(
  240. headers: {"X-Auth-Token": Configuration.instance.getToken()},
  241. ),
  242. )
  243. .then((_) async {
  244. final encryptedFile = io.File(temporaryPath);
  245. final thumbnailDecryptionKey = decryptFileKey(file);
  246. var data = CryptoUtil.decryptChaCha(
  247. encryptedFile.readAsBytesSync(),
  248. thumbnailDecryptionKey,
  249. Sodium.base642bin(file.thumbnailDecryptionHeader),
  250. );
  251. final thumbnailSize = data.length;
  252. if (thumbnailSize > THUMBNAIL_DATA_LIMIT) {
  253. data = await compressThumbnail(data);
  254. _logger.info("Compressed thumbnail from " +
  255. thumbnailSize.toString() +
  256. " to " +
  257. data.length.toString());
  258. }
  259. encryptedFile.deleteSync();
  260. final cachedThumbnail = ThumbnailCacheManager().putFile(
  261. file.getThumbnailUrl(),
  262. data,
  263. eTag: file.getThumbnailUrl(),
  264. maxAge: Duration(days: 365),
  265. );
  266. thumbnailDownloadsInProgress.remove(file.uploadedFileID);
  267. return cachedThumbnail;
  268. }).catchError((e, s) {
  269. _logger.severe("Error downloading thumbnail ", e, s);
  270. thumbnailDownloadsInProgress.remove(file.uploadedFileID);
  271. });
  272. }
  273. Uint8List decryptFileKey(File file) {
  274. final encryptedKey = Sodium.base642bin(file.encryptedKey);
  275. final nonce = Sodium.base642bin(file.keyDecryptionNonce);
  276. final collectionKey =
  277. CollectionsService.instance.getCollectionKey(file.collectionID);
  278. return CryptoUtil.decryptSync(encryptedKey, collectionKey, nonce);
  279. }
  280. Future<Uint8List> compressThumbnail(Uint8List thumbnail) {
  281. return FlutterImageCompress.compressWithList(
  282. thumbnail,
  283. minHeight: COMPRESSED_THUMBNAIL_RESOLUTION,
  284. minWidth: COMPRESSED_THUMBNAIL_RESOLUTION,
  285. quality: 25,
  286. );
  287. }