multipart_upload_util.dart 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. // ignore_for_file: implementation_imports
  2. import "dart:io";
  3. import "dart:typed_data";
  4. import "package:dio/dio.dart";
  5. import "package:logging/logging.dart";
  6. import "package:photos/core/constants.dart";
  7. import "package:photos/core/network/network.dart";
  8. import "package:photos/db/upload_locks_db.dart";
  9. import "package:photos/services/feature_flag_service.dart";
  10. import "package:photos/utils/crypto_util.dart";
  11. import "package:photos/utils/xml_parser_util.dart";
  12. final _enteDio = NetworkClient.instance.enteDio;
  13. final _dio = NetworkClient.instance.getDio();
  14. final _uploadLocksDb = UploadLocksDB.instance;
  15. class PartETag extends XmlParsableObject {
  16. final int partNumber;
  17. final String eTag;
  18. PartETag(this.partNumber, this.eTag);
  19. @override
  20. String get elementName => "Part";
  21. @override
  22. Map<String, dynamic> toMap() {
  23. return {
  24. "PartNumber": partNumber,
  25. "ETag": eTag,
  26. };
  27. }
  28. }
  29. class MultipartUploadURLs {
  30. final String objectKey;
  31. final List<String> partsURLs;
  32. final String completeURL;
  33. final List<bool>? partUploadStatus;
  34. final Map<int, String>? partETags;
  35. MultipartUploadURLs({
  36. required this.objectKey,
  37. required this.partsURLs,
  38. required this.completeURL,
  39. this.partUploadStatus,
  40. this.partETags,
  41. });
  42. factory MultipartUploadURLs.fromMap(Map<String, dynamic> map) {
  43. return MultipartUploadURLs(
  44. objectKey: map["urls"]["objectKey"],
  45. partsURLs: (map["urls"]["partURLs"] as List).cast<String>(),
  46. completeURL: map["urls"]["completeURL"],
  47. );
  48. }
  49. }
  50. Future<int> calculatePartCount(int fileSize) async {
  51. final partCount = (fileSize / multipartPartSizeForUpload).ceil();
  52. return partCount;
  53. }
  54. Future<MultipartUploadURLs> getMultipartUploadURLs(int count) async {
  55. try {
  56. assert(
  57. FeatureFlagService.instance.isInternalUserOrDebugBuild(),
  58. "Multipart upload should not be enabled for external users.",
  59. );
  60. final response = await _enteDio.get(
  61. "/files/multipart-upload-urls",
  62. queryParameters: {
  63. "count": count,
  64. },
  65. );
  66. return MultipartUploadURLs.fromMap(response.data);
  67. } on Exception catch (e) {
  68. Logger("MultipartUploadURL").severe(e);
  69. rethrow;
  70. }
  71. }
  72. Future<void> createTableEntry(
  73. String localId,
  74. String fileHash,
  75. MultipartUploadURLs urls,
  76. String encryptedFilePath,
  77. int fileSize,
  78. Uint8List fileKey,
  79. ) async {
  80. await _uploadLocksDb.createTrackUploadsEntry(
  81. localId,
  82. fileHash,
  83. urls,
  84. encryptedFilePath,
  85. fileSize,
  86. CryptoUtil.bin2base64(fileKey),
  87. );
  88. }
  89. Future<String> putExistingMultipartFile(
  90. File encryptedFile,
  91. String localId,
  92. String fileHash,
  93. ) async {
  94. final (urls, status) = await _uploadLocksDb.getCachedLinks(localId, fileHash);
  95. Map<int, String> etags = urls.partETags ?? {};
  96. if (status == UploadLocksDB.trackStatus.pending) {
  97. // upload individual parts and get their etags
  98. etags = await uploadParts(urls, encryptedFile);
  99. }
  100. if (status != UploadLocksDB.trackStatus.completed) {
  101. // complete the multipart upload
  102. await completeMultipartUpload(urls.objectKey, etags, urls.completeURL);
  103. }
  104. return urls.objectKey;
  105. }
  106. Future<String> putMultipartFile(
  107. MultipartUploadURLs urls,
  108. File encryptedFile,
  109. ) async {
  110. // upload individual parts and get their etags
  111. final etags = await uploadParts(urls, encryptedFile);
  112. // complete the multipart upload
  113. await completeMultipartUpload(urls.objectKey, etags, urls.completeURL);
  114. return urls.objectKey;
  115. }
  116. Future<Map<int, String>> uploadParts(
  117. MultipartUploadURLs url,
  118. File encryptedFile,
  119. ) async {
  120. final partsURLs = url.partsURLs;
  121. final partUploadStatus = url.partUploadStatus;
  122. final partsLength = partsURLs.length;
  123. final etags = url.partETags ?? <int, String>{};
  124. for (int i = 0; i < partsLength; i++) {
  125. if (i < (partUploadStatus?.length ?? 0) &&
  126. (partUploadStatus?[i] ?? false)) {
  127. continue;
  128. }
  129. final partURL = partsURLs[i];
  130. final isLastPart = i == partsLength - 1;
  131. final fileSize = isLastPart
  132. ? encryptedFile.lengthSync() % multipartPartSizeForUpload
  133. : multipartPartSizeForUpload;
  134. final response = await _dio.put(
  135. partURL,
  136. data: encryptedFile.openRead(
  137. i * multipartPartSizeForUpload,
  138. isLastPart ? null : (i + 1) * multipartPartSizeForUpload,
  139. ),
  140. options: Options(
  141. headers: {
  142. Headers.contentLengthHeader: fileSize,
  143. },
  144. ),
  145. );
  146. final eTag = response.headers.value("etag");
  147. if (eTag?.isEmpty ?? true) {
  148. throw Exception('ETAG_MISSING');
  149. }
  150. etags[i] = eTag!;
  151. await _uploadLocksDb.updatePartStatus(url.objectKey, i, eTag);
  152. }
  153. await _uploadLocksDb.updateTrackUploadStatus(
  154. url.objectKey,
  155. UploadLocksDB.trackStatus.uploaded,
  156. );
  157. return etags;
  158. }
  159. Future<void> completeMultipartUpload(
  160. String objectKey,
  161. Map<int, String> partEtags,
  162. String completeURL,
  163. ) async {
  164. final body = convertJs2Xml({
  165. 'CompleteMultipartUpload': partEtags.entries
  166. .map(
  167. (e) => PartETag(
  168. e.key + 1,
  169. e.value,
  170. ),
  171. )
  172. .toList(),
  173. }).replaceAll('"', '').replaceAll('&quot;', '');
  174. try {
  175. await _dio.post(
  176. completeURL,
  177. data: body,
  178. options: Options(
  179. contentType: "text/xml",
  180. ),
  181. );
  182. await _uploadLocksDb.updateTrackUploadStatus(
  183. objectKey,
  184. UploadLocksDB.trackStatus.completed,
  185. );
  186. } catch (e) {
  187. Logger("MultipartUpload").severe(e);
  188. rethrow;
  189. }
  190. }