import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:core/data/network/dio_client.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/file_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:get/get_connect/http/src/request/request.dart'; import 'package:model/email/attachment.dart'; import 'package:model/upload/file_info.dart'; import 'package:model/upload/upload_response.dart'; import 'package:tmail_ui_user/features/base/isolate/background_isolate_binary_messenger/background_isolate_binary_messenger.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; import 'package:tmail_ui_user/features/upload/data/model/upload_file_arguments.dart'; import 'package:tmail_ui_user/features/upload/domain/exceptions/upload_exception.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/main/exceptions/isolate_exception.dart'; import 'package:worker_manager/worker_manager.dart' as worker; class FileUploader { static const String uploadAttachmentExtraKey = 'upload-attachment'; static const String streamDataExtraKey = 'streamData'; static const String filePathExtraKey = 'path'; final DioClient _dioClient; final FileUtils _fileUtils; FileUploader( this._dioClient, this._fileUtils, ); Future uploadAttachment( UploadTaskId uploadId, FileInfo fileInfo, Uri uploadUri, { CancelToken? cancelToken, StreamController>? onSendController, } ) async { if (PlatformInfo.isWeb || Platform.numberOfProcessors == 1) { return _handleUploadAttachmentActionOnMainIsolate( uploadId, fileInfo, uploadUri, cancelToken: cancelToken, onSendController: onSendController, ); } else { final rootIsolateToken = RootIsolateToken.instance; if (rootIsolateToken == null) { throw const CanNotGetRootIsolateToken(); } final args = UploadFileArguments( _dioClient, _fileUtils, uploadId, fileInfo, uploadUri, rootIsolateToken, ); return await worker.workerManager.executeWithPort( _buildUploadClosure(args), onMessage: (value) { log('FileUploader::uploadAttachment(): onUpdateProgress: $value'); onSendController?.add(Right(value)); }, ) .then((value) => value) .catchError((error) => throw error); } } static Future Function(worker.SendPort) _buildUploadClosure( UploadFileArguments args, ) => (sendPort) => _handleUploadAttachmentAction(args, sendPort); static Future _handleUploadAttachmentAction( UploadFileArguments argsUpload, worker.SendPort sendPort, ) async { try { final rootIsolateToken = argsUpload.isolateToken; BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); await HiveCacheConfig.instance.setUp(); final headerParam = argsUpload.dioClient.getHeaders(); headerParam[HttpHeaders.contentTypeHeader] = argsUpload.fileInfo.mimeType; headerParam[HttpHeaders.contentLengthHeader] = argsUpload.fileInfo.fileSize; final mapExtra = { uploadAttachmentExtraKey: { if (argsUpload.fileInfo.filePath?.isNotEmpty == true) filePathExtraKey: argsUpload.fileInfo.filePath, if (argsUpload.fileInfo.bytes?.isNotEmpty == true) streamDataExtraKey: BodyBytesStream.fromBytes(argsUpload.fileInfo.bytes!), } }; final resultJson = await argsUpload.dioClient.post( Uri.decodeFull(argsUpload.uploadUri.toString()), options: Options( headers: headerParam, extra: mapExtra ), data: argsUpload.fileInfo.filePath?.isNotEmpty == true ? File(argsUpload.fileInfo.filePath!).openRead() : argsUpload.fileInfo.bytes != null ? BodyBytesStream.fromBytes(argsUpload.fileInfo.bytes!) : null, onSendProgress: (count, total) { log('FileUploader::_handleUploadAttachmentAction():onSendProgress: FILE[${argsUpload.uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); sendPort.send( UploadingAttachmentUploadState( argsUpload.uploadId, count, argsUpload.fileInfo.fileSize ) ); } ); log('FileUploader::_handleUploadAttachmentAction(): RESULT_JSON = $resultJson'); if (argsUpload.fileInfo.mimeType == FileUtils.TEXT_PLAIN_MIME_TYPE) { final fileBytes = argsUpload.fileInfo.filePath?.isNotEmpty == true ? File(argsUpload.fileInfo.filePath!).readAsBytesSync() : argsUpload.fileInfo.bytes; final fileCharset = fileBytes != null ? await argsUpload.fileUtils.getCharsetFromBytes(fileBytes) : null; return _parsingResponse( resultJson: resultJson, fileName: argsUpload.fileInfo.fileName, fileCharset: fileCharset?.toLowerCase()); } else { return _parsingResponse( resultJson: resultJson, fileName: argsUpload.fileInfo.fileName); } } on DioException catch (exception) { logWarning('FileUploader::_handleUploadAttachmentAction():DioException: $exception'); throw exception.copyWith( requestOptions: exception.requestOptions.copyWith(data: '')); } catch (exception) { logWarning('FileUploader::_handleUploadAttachmentAction():OtherException: $exception'); rethrow; } } Future _handleUploadAttachmentActionOnMainIsolate( UploadTaskId uploadId, FileInfo fileInfo, Uri uploadUri, { CancelToken? cancelToken, StreamController>? onSendController, } ) async { final headerParam = _dioClient.getHeaders(); headerParam[HttpHeaders.contentTypeHeader] = fileInfo.mimeType; headerParam[HttpHeaders.contentLengthHeader] = fileInfo.fileSize; final mapExtra = { uploadAttachmentExtraKey: { if (fileInfo.bytes?.isNotEmpty == true) streamDataExtraKey: BodyBytesStream.fromBytes(fileInfo.bytes!), } }; final resultJson = await _dioClient.post( Uri.decodeFull(uploadUri.toString()), options: Options( headers: headerParam, extra: mapExtra ), data: BodyBytesStream.fromBytes(fileInfo.bytes!), cancelToken: cancelToken, onSendProgress: (count, total) { log('FileUploader::_handleUploadAttachmentActionOnMainIsolate():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); onSendController?.add( Right(UploadingAttachmentUploadState( uploadId, count, fileInfo.fileSize )) ); } ); log('FileUploader::_handleUploadAttachmentActionOnMainIsolate(): RESULT_JSON = $resultJson'); if (fileInfo.mimeType == FileUtils.TEXT_PLAIN_MIME_TYPE) { final fileCharset = await _fileUtils.getCharsetFromBytes(fileInfo.bytes!); return _parsingResponse( resultJson: resultJson, fileName: fileInfo.fileName, fileCharset: fileCharset.toLowerCase()); } else { return _parsingResponse( resultJson: resultJson, fileName: fileInfo.fileName); } } static Attachment _parsingResponse({ dynamic resultJson, required String fileName, String? fileCharset }) { if (resultJson != null) { final decodeJson = resultJson is Map ? resultJson : jsonDecode(resultJson); final uploadResponse = UploadResponse.fromJson(decodeJson); log('FileUploader::_parsingResponse(): UploadResponse = $uploadResponse'); return uploadResponse.toAttachment( nameFile: fileName, charset: fileCharset); } else { logWarning('FileUploader::_parsingResponse(): DataResponseIsNullException'); throw const DataResponseIsNullException(); } } }