// ignore_for_file: implementation_imports import "dart:io"; import "package:dio/dio.dart"; import "package:logging/logging.dart"; import "package:photos/core/constants.dart"; import "package:photos/core/network/network.dart"; import "package:xml/src/xml/entities/named_entities.dart"; import "package:xml/xml.dart"; final _enteDio = NetworkClient.instance.enteDio; final _dio = NetworkClient.instance.getDio(); class MultipartUploadURLs { final String objectKey; final List partsURLs; final String completeURL; MultipartUploadURLs({ required this.objectKey, required this.partsURLs, required this.completeURL, }); factory MultipartUploadURLs.fromMap(Map map) { return MultipartUploadURLs( objectKey: map["urls"]["objectKey"], partsURLs: (map["urls"]["partURLs"] as List).cast(), completeURL: map["urls"]["completeURL"], ); } } Future calculatePartCount(int fileSize) async { final partCount = (fileSize / multipartPartSize).ceil(); return partCount; } Future getMultipartUploadURLs(int count) async { try { final response = await _enteDio.get( "/files/multipart-upload-urls", queryParameters: { "count": count, }, ); return MultipartUploadURLs.fromMap(response.data); } on Exception catch (e) { Logger("MultipartUploadURL").severe(e); rethrow; } } Future putMultipartFile( MultipartUploadURLs urls, File encryptedFile, ) async { // upload individual parts and get their etags final etags = await uploadParts(urls.partsURLs, encryptedFile); print(etags); // complete the multipart upload await completeMultipartUpload(etags, urls.completeURL); } Future> uploadParts( List partsURLs, File encryptedFile, ) async { final partsLength = partsURLs.length; final etags = {}; for (int i = 0; i < partsLength; i++) { final partURL = partsURLs[i]; final isLastPart = i == partsLength - 1; final fileSize = isLastPart ? encryptedFile.lengthSync() % multipartPartSize : multipartPartSize; final response = await _dio.put( partURL, data: encryptedFile.openRead( i * multipartPartSize, isLastPart ? null : multipartPartSize, ), options: Options( headers: { Headers.contentLengthHeader: fileSize, }, ), ); final eTag = response.headers.value("etag"); if (eTag?.isEmpty ?? true) { throw Exception('ETAG_MISSING'); } etags[i] = eTag!; } return etags; } Future completeMultipartUpload( Map partEtags, String completeURL, ) async { final body = convertJs2Xml({ 'CompleteMultipartUpload': partEtags.entries.toList(), }); print(body); try { await _dio.post( completeURL, data: body, options: Options( contentType: "text/xml", ), ); } catch (e) { Logger("MultipartUpload").severe(e); rethrow; } } // for converting the response to xml String convertJs2Xml(Map json) { final builder = XmlBuilder(); buildXml(builder, json); return builder.buildDocument().toXmlString( pretty: true, indent: ' ', entityMapping: defaultMyEntityMapping, ); } void buildXml(XmlBuilder builder, dynamic node) { if (node is Map) { node.forEach((key, value) { builder.element(key, nest: () => buildXml(builder, value)); }); } else if (node is List) { for (var item in node) { buildXml(builder, item); } } else { builder.element( "Part", nest: () { builder.attribute( "PartNumber", (node as MapEntry).key + 1, ); print(node.value); builder.attribute("ETag", node.value); }, ); } } XmlEntityMapping defaultMyEntityMapping = MyXmlDefaultEntityMapping.xml(); class MyXmlDefaultEntityMapping extends XmlDefaultEntityMapping { MyXmlDefaultEntityMapping.xml() : this(xmlEntities); MyXmlDefaultEntityMapping.html() : this(htmlEntities); MyXmlDefaultEntityMapping.html5() : this(html5Entities); MyXmlDefaultEntityMapping(super.entities); @override String encodeText(String input) => input.replaceAllMapped(_textPattern, _textReplace); @override String encodeAttributeValue(String input, XmlAttributeType type) { switch (type) { case XmlAttributeType.SINGLE_QUOTE: return input.replaceAllMapped( _singeQuoteAttributePattern, _singeQuoteAttributeReplace, ); case XmlAttributeType.DOUBLE_QUOTE: return input.replaceAllMapped( _doubleQuoteAttributePattern, _doubleQuoteAttributeReplace, ); } } } final _textPattern = RegExp(r'[&<>' + _highlyDiscouragedCharClass + r']'); String _textReplace(Match match) { final toEscape = match.group(0)!; switch (toEscape) { case '<': return '<'; case '&': return '&'; case '>': return '>'; default: return _asNumericCharacterReferences(toEscape); } } final _singeQuoteAttributePattern = RegExp(r"['&<>\n\r\t" + _highlyDiscouragedCharClass + r']'); String _singeQuoteAttributeReplace(Match match) { final toEscape = match.group(0)!; switch (toEscape) { case "'": return ''; case '&': return '&'; case '<': return '<'; case '>': return '>'; default: return _asNumericCharacterReferences(toEscape); } } final _doubleQuoteAttributePattern = RegExp(r'["&<>\n\r\t' + _highlyDiscouragedCharClass + r']'); String _doubleQuoteAttributeReplace(Match match) { final toEscape = match.group(0)!; switch (toEscape) { case '"': return ''; case '&': return '&'; case '<': return '<'; case '>': return '>'; default: return _asNumericCharacterReferences(toEscape); } } const _highlyDiscouragedCharClass = r'\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f-\u0084\u0086-\u009f'; String _asNumericCharacterReferences(String toEscape) => toEscape.runes .map((rune) => '&#x${rune.toRadixString(16).toUpperCase()};') .join();