file_uploader.dart 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import 'dart:async';
  2. import 'dart:collection';
  3. import 'dart:convert';
  4. import 'dart:io' as io;
  5. import 'package:dio/dio.dart';
  6. import 'package:flutter_sodium/flutter_sodium.dart';
  7. import 'package:logging/logging.dart';
  8. import 'package:photos/core/configuration.dart';
  9. import 'package:photos/core/constants.dart';
  10. import 'package:photos/models/file.dart';
  11. import 'package:photos/models/location.dart';
  12. import 'package:photos/models/upload_url.dart';
  13. import 'package:photos/services/collections_service.dart';
  14. import 'package:photos/utils/crypto_util.dart';
  15. class FileUploader {
  16. final _logger = Logger("FileUploader");
  17. final _dio = Dio();
  18. final _queue = LinkedHashMap<int, FileUploadItem>();
  19. final _maximumConcurrentUploads = 4;
  20. int _currentlyUploading = 0;
  21. FileUploader._privateConstructor();
  22. static FileUploader instance = FileUploader._privateConstructor();
  23. Future<File> addToQueue(File file) {
  24. if (_queue[file.generatedID] == null) {
  25. _queue[file.generatedID] = FileUploadItem(file, Completer<File>());
  26. _pollQueue();
  27. }
  28. return _queue[file.generatedID].completer.future;
  29. }
  30. Future<File> getCurrentUploadStatus(File file) {
  31. return _queue[file.generatedID]?.completer?.future;
  32. }
  33. Future<File> forceUpload(File file) async {
  34. return _encryptAndUploadFile(file, forcedUpload: true);
  35. }
  36. void _pollQueue() {
  37. if (_queue.length > 0 && _currentlyUploading < _maximumConcurrentUploads) {
  38. final firstPendingEntry = _queue.entries
  39. .firstWhere((entry) => entry.value.status == UploadStatus.not_started)
  40. .value;
  41. firstPendingEntry.status = UploadStatus.in_progress;
  42. _encryptAndUploadFile(firstPendingEntry.file);
  43. }
  44. }
  45. Future<File> _encryptAndUploadFile(File file,
  46. {bool forcedUpload = false}) async {
  47. _logger.info("Uploading " + file.toString());
  48. if (!forcedUpload) {
  49. _currentlyUploading++;
  50. }
  51. final encryptedFileName = file.generatedID.toString() + ".encrypted";
  52. final tempDirectory = Configuration.instance.getTempDirectory();
  53. final encryptedFilePath = tempDirectory + encryptedFileName;
  54. final sourceFile = (await (await file.getAsset()).originFile);
  55. final encryptedFile = io.File(encryptedFilePath);
  56. final fileAttributes =
  57. await CryptoUtil.encryptFile(sourceFile.path, encryptedFilePath);
  58. final fileUploadURL = await _getUploadURL();
  59. String fileObjectKey = await _putFile(fileUploadURL, encryptedFile);
  60. final thumbnailData = (await (await file.getAsset()).thumbDataWithSize(
  61. THUMBNAIL_LARGE_SIZE,
  62. THUMBNAIL_LARGE_SIZE,
  63. quality: 50,
  64. ));
  65. final encryptedThumbnailName =
  66. file.generatedID.toString() + "_thumbnail.encrypted";
  67. final encryptedThumbnailPath = tempDirectory + encryptedThumbnailName;
  68. final encryptedThumbnailData =
  69. CryptoUtil.encryptChaCha(thumbnailData, fileAttributes.key);
  70. final encryptedThumbnail = io.File(encryptedThumbnailPath);
  71. encryptedThumbnail.writeAsBytesSync(encryptedThumbnailData.encryptedData);
  72. final thumbnailUploadURL = await _getUploadURL();
  73. String thumbnailObjectKey =
  74. await _putFile(thumbnailUploadURL, encryptedThumbnail);
  75. // h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads
  76. if (file.location.latitude == 0 && file.location.longitude == 0) {
  77. final latLong = await (await file.getAsset()).latlngAsync();
  78. file.location = Location(latLong.latitude, latLong.longitude);
  79. }
  80. final encryptedMetadataData = CryptoUtil.encryptChaCha(
  81. utf8.encode(jsonEncode(file.getMetadata())), fileAttributes.key);
  82. final encryptedFileKeyData = CryptoUtil.encryptSync(
  83. fileAttributes.key,
  84. CollectionsService.instance.getCollectionKey(file.collectionID),
  85. );
  86. final encryptedKey = Sodium.bin2base64(encryptedFileKeyData.encryptedData);
  87. final keyDecryptionNonce = Sodium.bin2base64(encryptedFileKeyData.nonce);
  88. final fileDecryptionHeader = Sodium.bin2base64(fileAttributes.header);
  89. final thumbnailDecryptionHeader =
  90. Sodium.bin2base64(encryptedThumbnailData.header);
  91. final encryptedMetadata =
  92. Sodium.bin2base64(encryptedMetadataData.encryptedData);
  93. final metadataDecryptionHeader =
  94. Sodium.bin2base64(encryptedMetadataData.header);
  95. final data = {
  96. "collectionID": file.collectionID,
  97. "encryptedKey": encryptedKey,
  98. "keyDecryptionNonce": keyDecryptionNonce,
  99. "file": {
  100. "objectKey": fileObjectKey,
  101. "decryptionHeader": fileDecryptionHeader,
  102. },
  103. "thumbnail": {
  104. "objectKey": thumbnailObjectKey,
  105. "decryptionHeader": thumbnailDecryptionHeader,
  106. },
  107. "metadata": {
  108. "encryptedData": encryptedMetadata,
  109. "decryptionHeader": metadataDecryptionHeader,
  110. }
  111. };
  112. return _dio
  113. .post(
  114. Configuration.instance.getHttpEndpoint() + "/files",
  115. options:
  116. Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
  117. data: data,
  118. )
  119. .then((response) {
  120. encryptedFile.deleteSync();
  121. encryptedThumbnail.deleteSync();
  122. final data = response.data;
  123. file.uploadedFileID = data["id"];
  124. file.updationTime = data["updationTime"];
  125. file.ownerID = data["ownerID"];
  126. file.encryptedKey = encryptedKey;
  127. file.keyDecryptionNonce = keyDecryptionNonce;
  128. file.fileDecryptionHeader = fileDecryptionHeader;
  129. file.thumbnailDecryptionHeader = thumbnailDecryptionHeader;
  130. file.metadataDecryptionHeader = metadataDecryptionHeader;
  131. if (!forcedUpload) {
  132. _currentlyUploading--;
  133. _queue[file.generatedID].completer.complete(file);
  134. _queue.remove(file.generatedID);
  135. _pollQueue();
  136. }
  137. return file;
  138. });
  139. }
  140. Future<UploadURL> _getUploadURL() {
  141. return Dio()
  142. .get(
  143. Configuration.instance.getHttpEndpoint() + "/files/upload-url",
  144. options: Options(
  145. headers: {"X-Auth-Token": Configuration.instance.getToken()}),
  146. )
  147. .then((response) => UploadURL.fromMap(response.data));
  148. }
  149. Future<String> _putFile(UploadURL uploadURL, io.File file) async {
  150. final fileSize = file.lengthSync().toString();
  151. final startTime = DateTime.now().millisecondsSinceEpoch;
  152. _logger.info("Putting file of size " + fileSize + " to " + uploadURL.url);
  153. return Dio()
  154. .put(uploadURL.url,
  155. data: file.openRead(),
  156. options: Options(headers: {
  157. Headers.contentLengthHeader: await file.length(),
  158. }))
  159. .catchError((e) {
  160. _logger.severe(e);
  161. throw e;
  162. }).then((value) {
  163. _logger.info("Upload speed : " +
  164. (file.lengthSync() /
  165. (DateTime.now().millisecondsSinceEpoch - startTime))
  166. .toString() +
  167. " kilo bytes per second");
  168. return uploadURL.objectKey;
  169. });
  170. }
  171. }
  172. class FileUploadItem {
  173. final File file;
  174. final Completer<File> completer;
  175. UploadStatus status;
  176. FileUploadItem(
  177. this.file,
  178. this.completer, {
  179. this.status = UploadStatus.not_started,
  180. });
  181. }
  182. enum UploadStatus {
  183. not_started,
  184. in_progress,
  185. completed,
  186. }