file_uploader.dart 6.7 KB

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