multipart_upload_util.dart 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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/utils/crypto_util.dart";
  10. import "package:photos/utils/xml_parser_util.dart";
  11. final _enteDio = NetworkClient.instance.enteDio;
  12. final _dio = NetworkClient.instance.getDio();
  13. class PartETag extends XmlParsableObject {
  14. final int partNumber;
  15. final String eTag;
  16. PartETag(this.partNumber, this.eTag);
  17. @override
  18. String get elementName => "Part";
  19. @override
  20. Map<String, dynamic> toMap() {
  21. return {
  22. "PartNumber": partNumber,
  23. "ETag": eTag,
  24. };
  25. }
  26. }
  27. class MultipartUploadURLs {
  28. final String objectKey;
  29. final List<String> partsURLs;
  30. final String completeURL;
  31. final List<bool>? partUploadStatus;
  32. MultipartUploadURLs({
  33. required this.objectKey,
  34. required this.partsURLs,
  35. required this.completeURL,
  36. this.partUploadStatus,
  37. });
  38. factory MultipartUploadURLs.fromMap(Map<String, dynamic> map) {
  39. return MultipartUploadURLs(
  40. objectKey: map["urls"]["objectKey"],
  41. partsURLs: (map["urls"]["partURLs"] as List).cast<String>(),
  42. completeURL: map["urls"]["completeURL"],
  43. );
  44. }
  45. }
  46. Future<int> calculatePartCount(int fileSize) async {
  47. final partCount = (fileSize / multipartPartSize).ceil();
  48. return partCount;
  49. }
  50. Future<MultipartUploadURLs> getMultipartUploadURLs(int count) async {
  51. try {
  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<void> createTableEntry(
  65. String localId,
  66. String fileHash,
  67. MultipartUploadURLs urls,
  68. String encryptedFilePath,
  69. int fileSize,
  70. Uint8List fileKey,
  71. ) async {
  72. await UploadLocksDB.instance.createTrackUploadsEntry(
  73. localId,
  74. fileHash,
  75. urls,
  76. encryptedFilePath,
  77. fileSize,
  78. CryptoUtil.bin2base64(fileKey),
  79. );
  80. }
  81. Future<String> putExistingMultipartFile(
  82. File encryptedFile,
  83. String localId,
  84. String fileHash,
  85. ) async {
  86. final urls = await UploadLocksDB.instance.getCachedLinks(localId, fileHash);
  87. // upload individual parts and get their etags
  88. final etags = await uploadParts(urls, encryptedFile);
  89. // complete the multipart upload
  90. await completeMultipartUpload(urls.objectKey, etags, urls.completeURL);
  91. return urls.objectKey;
  92. }
  93. Future<String> putMultipartFile(
  94. MultipartUploadURLs urls,
  95. File encryptedFile,
  96. ) async {
  97. // upload individual parts and get their etags
  98. final etags = await uploadParts(urls, encryptedFile);
  99. // complete the multipart upload
  100. await completeMultipartUpload(urls.objectKey, etags, urls.completeURL);
  101. return urls.objectKey;
  102. }
  103. Future<Map<int, String>> uploadParts(
  104. MultipartUploadURLs url,
  105. File encryptedFile,
  106. ) async {
  107. final partsURLs = url.partsURLs;
  108. final partUploadStatus = url.partUploadStatus;
  109. final partsLength = partsURLs.length;
  110. final etags = <int, String>{};
  111. for (int i = 0; i < partsLength; i++) {
  112. if (i < (partUploadStatus?.length ?? 0) &&
  113. (partUploadStatus?[i] ?? false)) {
  114. continue;
  115. }
  116. final partURL = partsURLs[i];
  117. final isLastPart = i == partsLength - 1;
  118. final fileSize = isLastPart
  119. ? encryptedFile.lengthSync() % multipartPartSize
  120. : multipartPartSize;
  121. final response = await _dio.put(
  122. partURL,
  123. data: encryptedFile.openRead(
  124. i * multipartPartSize,
  125. isLastPart ? null : (i + 1) * multipartPartSize - 1,
  126. ),
  127. options: Options(
  128. headers: {
  129. Headers.contentLengthHeader: fileSize,
  130. },
  131. ),
  132. );
  133. final eTag = response.headers.value("etag");
  134. if (eTag?.isEmpty ?? true) {
  135. throw Exception('ETAG_MISSING');
  136. }
  137. etags[i] = eTag!;
  138. await UploadLocksDB.instance.updatePartStatus(url.objectKey, i);
  139. }
  140. return etags;
  141. }
  142. Future<void> completeMultipartUpload(
  143. String objectKey,
  144. Map<int, String> partEtags,
  145. String completeURL,
  146. ) async {
  147. final body = convertJs2Xml({
  148. 'CompleteMultipartUpload': partEtags.entries
  149. .map(
  150. (e) => PartETag(
  151. e.key + 1,
  152. e.value,
  153. ),
  154. )
  155. .toList(),
  156. }).replaceAll('"', '').replaceAll('&quot;', '');
  157. try {
  158. await _dio.post(
  159. completeURL,
  160. data: body,
  161. options: Options(
  162. contentType: "text/xml",
  163. ),
  164. );
  165. await UploadLocksDB.instance.updateCompletionStatus(objectKey);
  166. } catch (e) {
  167. Logger("MultipartUpload").severe(e);
  168. rethrow;
  169. }
  170. }