file_uploader_util.dart 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import 'dart:async';
  2. import 'dart:io' as io;
  3. import 'dart:typed_data';
  4. import 'package:archive/archive_io.dart';
  5. import 'package:flutter_sodium/flutter_sodium.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:motionphoto/motionphoto.dart';
  8. import 'package:path/path.dart';
  9. import 'package:path_provider/path_provider.dart';
  10. import 'package:photo_manager/photo_manager.dart';
  11. import 'package:photos/core/configuration.dart';
  12. import 'package:photos/core/constants.dart';
  13. import 'package:photos/core/errors.dart';
  14. import 'package:photos/models/file.dart' as ente;
  15. import 'package:photos/models/file_type.dart';
  16. import 'package:photos/models/location.dart';
  17. import 'package:photos/utils/crypto_util.dart';
  18. import 'package:photos/utils/file_util.dart';
  19. import 'package:video_thumbnail/video_thumbnail.dart';
  20. final _logger = Logger("FileUtil");
  21. const kMaximumThumbnailCompressionAttempts = 2;
  22. class MediaUploadData {
  23. final io.File sourceFile;
  24. final Uint8List thumbnail;
  25. final bool isDeleted;
  26. // presents the hash for the original video or image file.
  27. // for livePhotos, fileHash represents the image hash value
  28. final String fileHash;
  29. final String liveVideoHash;
  30. final String zipHash;
  31. MediaUploadData(
  32. this.sourceFile,
  33. this.thumbnail,
  34. this.isDeleted, {
  35. this.fileHash,
  36. this.liveVideoHash,
  37. this.zipHash,
  38. });
  39. }
  40. Future<MediaUploadData> getUploadDataFromEnteFile(ente.File file) async {
  41. if (file.isSharedMediaToAppSandbox()) {
  42. return await _getMediaUploadDataFromAppCache(file);
  43. } else {
  44. return await _getMediaUploadDataFromAssetFile(file);
  45. }
  46. }
  47. Future<MediaUploadData> _getMediaUploadDataFromAssetFile(ente.File file) async {
  48. io.File sourceFile;
  49. Uint8List thumbnailData;
  50. bool isDeleted;
  51. String fileHash, livePhotoVideoHash, zipHash;
  52. // The timeouts are to safeguard against https://github.com/CaiJingLong/flutter_photo_manager/issues/467
  53. final asset = await file
  54. .getAsset()
  55. .timeout(const Duration(seconds: 3))
  56. .catchError((e) async {
  57. if (e is TimeoutException) {
  58. _logger.info("Asset fetch timed out for " + file.toString());
  59. return await file.getAsset();
  60. } else {
  61. throw e;
  62. }
  63. });
  64. if (asset == null) {
  65. throw InvalidFileError("asset is null");
  66. }
  67. sourceFile = await asset.originFile
  68. .timeout(const Duration(seconds: 3))
  69. .catchError((e) async {
  70. if (e is TimeoutException) {
  71. _logger.info("Origin file fetch timed out for " + file.toString());
  72. return await asset.originFile;
  73. } else {
  74. throw e;
  75. }
  76. });
  77. if (sourceFile == null || !sourceFile.existsSync()) {
  78. throw InvalidFileError("source fill is null or do not exist");
  79. }
  80. // h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads
  81. await _decorateEnteFileData(file, asset);
  82. fileHash = Sodium.bin2base64(await CryptoUtil.getHash(sourceFile));
  83. if (file.fileType == FileType.livePhoto && io.Platform.isIOS) {
  84. final io.File videoUrl = await Motionphoto.getLivePhotoFile(file.localID);
  85. if (videoUrl == null || !videoUrl.existsSync()) {
  86. String errMsg =
  87. "missing livePhoto url for ${file.toString()} with subType ${file.fileSubType}";
  88. _logger.severe(errMsg);
  89. throw InvalidFileUploadState(errMsg);
  90. }
  91. livePhotoVideoHash = Sodium.bin2base64(await CryptoUtil.getHash(videoUrl));
  92. final tempPath = Configuration.instance.getTempDirectory();
  93. // .elp -> ente live photo
  94. final livePhotoPath = tempPath + file.generatedID.toString() + ".elp";
  95. _logger.fine("Uploading zipped live photo from " + livePhotoPath);
  96. var encoder = ZipFileEncoder();
  97. encoder.create(livePhotoPath);
  98. encoder.addFile(videoUrl, "video" + extension(videoUrl.path));
  99. encoder.addFile(sourceFile, "image" + extension(sourceFile.path));
  100. encoder.close();
  101. // delete the temporary video and image copy (only in IOS)
  102. if (io.Platform.isIOS) {
  103. await sourceFile.delete();
  104. }
  105. // new sourceFile which needs to be uploaded
  106. sourceFile = io.File(livePhotoPath);
  107. zipHash = Sodium.bin2base64(await CryptoUtil.getHash(sourceFile));
  108. }
  109. thumbnailData = await asset.thumbnailDataWithSize(
  110. const ThumbnailSize(kThumbnailLargeSize, kThumbnailLargeSize),
  111. quality: kThumbnailQuality,
  112. );
  113. if (thumbnailData == null) {
  114. throw InvalidFileError("unable to get asset thumbData");
  115. }
  116. int compressionAttempts = 0;
  117. while (thumbnailData.length > kThumbnailDataLimit &&
  118. compressionAttempts < kMaximumThumbnailCompressionAttempts) {
  119. _logger.info("Thumbnail size " + thumbnailData.length.toString());
  120. thumbnailData = await compressThumbnail(thumbnailData);
  121. _logger
  122. .info("Compressed thumbnail size " + thumbnailData.length.toString());
  123. compressionAttempts++;
  124. }
  125. isDeleted = asset == null || !(await asset.exists);
  126. return MediaUploadData(
  127. sourceFile,
  128. thumbnailData,
  129. isDeleted,
  130. fileHash: fileHash,
  131. liveVideoHash: livePhotoVideoHash,
  132. zipHash: zipHash,
  133. );
  134. }
  135. Future<void> _decorateEnteFileData(ente.File file, AssetEntity asset) async {
  136. // h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads
  137. if (file.location == null ||
  138. (file.location.latitude == 0 && file.location.longitude == 0)) {
  139. final latLong = await asset.latlngAsync();
  140. file.location = Location(latLong.latitude, latLong.longitude);
  141. }
  142. if (file.title == null || file.title.isEmpty) {
  143. _logger.warning("Title was missing ${file.tag()}");
  144. file.title = await asset.titleAsync;
  145. }
  146. }
  147. Future<MediaUploadData> _getMediaUploadDataFromAppCache(ente.File file) async {
  148. io.File sourceFile;
  149. Uint8List thumbnailData;
  150. bool isDeleted = false;
  151. var localPath = getSharedMediaFilePath(file);
  152. sourceFile = io.File(localPath);
  153. if (!sourceFile.existsSync()) {
  154. _logger.warning("File doesn't exist in app sandbox");
  155. throw InvalidFileError("File doesn't exist in app sandbox");
  156. }
  157. try {
  158. thumbnailData = await getThumbnailFromInAppCacheFile(file);
  159. return MediaUploadData(sourceFile, thumbnailData, isDeleted);
  160. } catch (e, s) {
  161. _logger.severe("failed to generate thumbnail", e, s);
  162. throw InvalidFileError(
  163. "thumbnail generation failed for fileType: ${file.fileType.toString()}",
  164. );
  165. }
  166. }
  167. Future<Uint8List> getThumbnailFromInAppCacheFile(ente.File file) async {
  168. var localFile = io.File(getSharedMediaFilePath(file));
  169. if (!localFile.existsSync()) {
  170. return null;
  171. }
  172. if (file.fileType == FileType.video) {
  173. final thumbnailFilePath = await VideoThumbnail.thumbnailFile(
  174. video: localFile.path,
  175. imageFormat: ImageFormat.JPEG,
  176. thumbnailPath: (await getTemporaryDirectory()).path,
  177. maxWidth: kThumbnailLargeSize,
  178. quality: 80,
  179. );
  180. localFile = io.File(thumbnailFilePath);
  181. }
  182. var thumbnailData = await localFile.readAsBytes();
  183. int compressionAttempts = 0;
  184. while (thumbnailData.length > kThumbnailDataLimit &&
  185. compressionAttempts < kMaximumThumbnailCompressionAttempts) {
  186. _logger.info("Thumbnail size " + thumbnailData.length.toString());
  187. thumbnailData = await compressThumbnail(thumbnailData);
  188. _logger
  189. .info("Compressed thumbnail size " + thumbnailData.length.toString());
  190. compressionAttempts++;
  191. }
  192. return thumbnailData;
  193. }