multipart_upload_util.dart 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. // ignore_for_file: implementation_imports
  2. import "dart:io";
  3. import "package:dio/dio.dart";
  4. import "package:logging/logging.dart";
  5. import "package:photos/core/constants.dart";
  6. import "package:photos/core/network/network.dart";
  7. import "package:photos/services/feature_flag_service.dart";
  8. import "package:photos/utils/xml_parser_util.dart";
  9. final _enteDio = NetworkClient.instance.enteDio;
  10. final _dio = NetworkClient.instance.getDio();
  11. class PartETag extends XmlParsableObject {
  12. final int partNumber;
  13. final String eTag;
  14. PartETag(this.partNumber, this.eTag);
  15. @override
  16. String get elementName => "Part";
  17. @override
  18. Map<String, dynamic> toMap() {
  19. return {
  20. "PartNumber": partNumber,
  21. "ETag": eTag,
  22. };
  23. }
  24. }
  25. class MultipartUploadURLs {
  26. final String objectKey;
  27. final List<String> partsURLs;
  28. final String completeURL;
  29. MultipartUploadURLs({
  30. required this.objectKey,
  31. required this.partsURLs,
  32. required this.completeURL,
  33. });
  34. factory MultipartUploadURLs.fromMap(Map<String, dynamic> map) {
  35. return MultipartUploadURLs(
  36. objectKey: map["urls"]["objectKey"],
  37. partsURLs: (map["urls"]["partURLs"] as List).cast<String>(),
  38. completeURL: map["urls"]["completeURL"],
  39. );
  40. }
  41. }
  42. Future<int> calculatePartCount(int fileSize) async {
  43. final partCount = (fileSize / multipartPartSize).ceil();
  44. return partCount;
  45. }
  46. Future<MultipartUploadURLs> getMultipartUploadURLs(int count) async {
  47. try {
  48. assert(
  49. FeatureFlagService.instance.isInternalUserOrDebugBuild(),
  50. "Multipart upload should not be enabled for external users.",
  51. );
  52. final response = await _enteDio.get(
  53. "/files/multipart-upload-urls",
  54. queryParameters: {
  55. "count": count,
  56. },
  57. );
  58. return MultipartUploadURLs.fromMap(response.data);
  59. } on Exception catch (e) {
  60. Logger("MultipartUploadURL").severe(e);
  61. rethrow;
  62. }
  63. }
  64. Future<String> putMultipartFile(
  65. MultipartUploadURLs urls,
  66. File encryptedFile,
  67. ) async {
  68. // upload individual parts and get their etags
  69. final etags = await uploadParts(urls.partsURLs, encryptedFile);
  70. // complete the multipart upload
  71. await completeMultipartUpload(etags, urls.completeURL);
  72. return urls.objectKey;
  73. }
  74. Future<Map<int, String>> uploadParts(
  75. List<String> partsURLs,
  76. File encryptedFile,
  77. ) async {
  78. final partsLength = partsURLs.length;
  79. final etags = <int, String>{};
  80. for (int i = 0; i < partsLength; i++) {
  81. final partURL = partsURLs[i];
  82. final isLastPart = i == partsLength - 1;
  83. final fileSize = isLastPart
  84. ? encryptedFile.lengthSync() % multipartPartSize
  85. : multipartPartSize;
  86. final response = await _dio.put(
  87. partURL,
  88. data: encryptedFile.openRead(
  89. i * multipartPartSize,
  90. isLastPart ? null : multipartPartSize,
  91. ),
  92. options: Options(
  93. headers: {
  94. Headers.contentLengthHeader: fileSize,
  95. },
  96. ),
  97. );
  98. final eTag = response.headers.value("etag");
  99. if (eTag?.isEmpty ?? true) {
  100. throw Exception('ETAG_MISSING');
  101. }
  102. etags[i] = eTag!;
  103. }
  104. return etags;
  105. }
  106. Future<void> completeMultipartUpload(
  107. Map<int, String> partEtags,
  108. String completeURL,
  109. ) async {
  110. final body = convertJs2Xml({
  111. 'CompleteMultipartUpload': partEtags.entries
  112. .map(
  113. (e) => PartETag(
  114. e.key + 1,
  115. e.value,
  116. ),
  117. )
  118. .toList(),
  119. }).replaceAll('"', '').replaceAll('&quot;', '');
  120. try {
  121. await _dio.post(
  122. completeURL,
  123. data: body,
  124. options: Options(
  125. contentType: "text/xml",
  126. ),
  127. );
  128. } catch (e) {
  129. Logger("MultipartUpload").severe(e);
  130. rethrow;
  131. }
  132. }