// ignore_for_file: implementation_imports import "dart:io"; import "dart:typed_data"; 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:photos/db/upload_locks_db.dart"; import "package:photos/services/feature_flag_service.dart"; import "package:photos/utils/crypto_util.dart"; import "package:photos/utils/xml_parser_util.dart"; final _enteDio = NetworkClient.instance.enteDio; final _dio = NetworkClient.instance.getDio(); final _uploadLocksDb = UploadLocksDB.instance; class PartETag extends XmlParsableObject { final int partNumber; final String eTag; PartETag(this.partNumber, this.eTag); @override String get elementName => "Part"; @override Map toMap() { return { "PartNumber": partNumber, "ETag": eTag, }; } } class MultipartUploadURLs { final String objectKey; final List partsURLs; final String completeURL; final List? partUploadStatus; final Map? partETags; MultipartUploadURLs({ required this.objectKey, required this.partsURLs, required this.completeURL, this.partUploadStatus, this.partETags, }); 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 / multipartPartSizeForUpload).ceil(); return partCount; } Future getMultipartUploadURLs(int count) async { try { assert( FeatureFlagService.instance.isInternalUserOrDebugBuild(), "Multipart upload should not be enabled for external users.", ); 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 createTableEntry( String localId, String fileHash, MultipartUploadURLs urls, String encryptedFilePath, int fileSize, Uint8List fileKey, ) async { await _uploadLocksDb.createTrackUploadsEntry( localId, fileHash, urls, encryptedFilePath, fileSize, CryptoUtil.bin2base64(fileKey), ); } Future putExistingMultipartFile( File encryptedFile, String localId, String fileHash, ) async { final (urls, status) = await _uploadLocksDb.getCachedLinks(localId, fileHash); Map etags = urls.partETags ?? {}; if (status == UploadLocksDB.trackStatus.pending) { // upload individual parts and get their etags etags = await uploadParts(urls, encryptedFile); } if (status != UploadLocksDB.trackStatus.completed) { // complete the multipart upload await completeMultipartUpload(urls.objectKey, etags, urls.completeURL); } return urls.objectKey; } Future putMultipartFile( MultipartUploadURLs urls, File encryptedFile, ) async { // upload individual parts and get their etags final etags = await uploadParts(urls, encryptedFile); // complete the multipart upload await completeMultipartUpload(urls.objectKey, etags, urls.completeURL); return urls.objectKey; } Future> uploadParts( MultipartUploadURLs url, File encryptedFile, ) async { final partsURLs = url.partsURLs; final partUploadStatus = url.partUploadStatus; final partsLength = partsURLs.length; final etags = url.partETags ?? {}; for (int i = 0; i < partsLength; i++) { if (i < (partUploadStatus?.length ?? 0) && (partUploadStatus?[i] ?? false)) { continue; } final partURL = partsURLs[i]; final isLastPart = i == partsLength - 1; final fileSize = isLastPart ? encryptedFile.lengthSync() % multipartPartSizeForUpload : multipartPartSizeForUpload; final response = await _dio.put( partURL, data: encryptedFile.openRead( i * multipartPartSizeForUpload, isLastPart ? null : (i + 1) * multipartPartSizeForUpload, ), options: Options( headers: { Headers.contentLengthHeader: fileSize, }, ), ); final eTag = response.headers.value("etag"); if (eTag?.isEmpty ?? true) { throw Exception('ETAG_MISSING'); } etags[i] = eTag!; await _uploadLocksDb.updatePartStatus(url.objectKey, i, eTag); } await _uploadLocksDb.updateTrackUploadStatus( url.objectKey, UploadLocksDB.trackStatus.uploaded, ); return etags; } Future completeMultipartUpload( String objectKey, Map partEtags, String completeURL, ) async { final body = convertJs2Xml({ 'CompleteMultipartUpload': partEtags.entries .map( (e) => PartETag( e.key + 1, e.value, ), ) .toList(), }).replaceAll('"', '').replaceAll('"', ''); try { await _dio.post( completeURL, data: body, options: Options( contentType: "text/xml", ), ); await _uploadLocksDb.updateTrackUploadStatus( objectKey, UploadLocksDB.trackStatus.completed, ); } catch (e) { Logger("MultipartUpload").severe(e); rethrow; } }