diff --git a/lib/features/upload/data/model/upload_file_arguments.dart b/lib/features/upload/data/model/upload_file_arguments.dart new file mode 100644 index 000000000..b58004100 --- /dev/null +++ b/lib/features/upload/data/model/upload_file_arguments.dart @@ -0,0 +1,25 @@ + +import 'package:core/data/network/dio_client.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; + +class UploadFileArguments with EquatableMixin { + + final DioClient dioClient; + final UploadTaskId uploadId; + final FileInfo fileInfo; + final Uri uploadUri; + final CancelToken? cancelToken; + + UploadFileArguments( + this.dioClient, + this.uploadId, + this.fileInfo, + this.uploadUri, + {this.cancelToken}); + + @override + List get props => [uploadId, fileInfo, uploadUri]; +} \ No newline at end of file diff --git a/lib/features/upload/data/network/file_uploader.dart b/lib/features/upload/data/network/file_uploader.dart new file mode 100644 index 000000000..5006631a6 --- /dev/null +++ b/lib/features/upload/data/network/file_uploader.dart @@ -0,0 +1,137 @@ + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.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/upload/data/model/upload_file_arguments.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:worker_manager/worker_manager.dart' as worker; + +class FileUploader { + + final DioClient _dioClient; + final worker.Executor _isolateExecutor; + + FileUploader(this._dioClient, this._isolateExecutor); + + Future uploadAttachment( + UploadTaskId uploadId, + StreamController> onSendController, + FileInfo fileInfo, + Uri uploadUri, + {CancelToken? cancelToken} + ) async { + if (BuildUtils.isWeb) { + return _handleUploadAttachmentActionOnWeb( + uploadId, + onSendController, + fileInfo, + uploadUri, + cancelToken: cancelToken); + } else { + final attachmentUploaded = await _isolateExecutor.execute( + arg1: UploadFileArguments( + _dioClient, + uploadId, + fileInfo, + uploadUri, + cancelToken: cancelToken), + fun1: _handleUploadAttachmentAction, + notification: (value) { + if (value is Success) { + log('FileUploader::uploadAttachment(): onUpdateProgress: $value'); + onSendController.add(Right(value)); + } + }); + return attachmentUploaded; + } + } + + static Future _handleUploadAttachmentAction( + UploadFileArguments argsUpload, + worker.TypeSendPort sendPort + ) async { + try { + final dioClient = argsUpload.dioClient; + final fileInfo = argsUpload.fileInfo; + final uploadUri = argsUpload.uploadUri; + final cancelToken = argsUpload.cancelToken; + + final headerParam = dioClient.getHeaders(); + headerParam[HttpHeaders.contentTypeHeader] = fileInfo.mimeType; + headerParam[HttpHeaders.contentLengthHeader] = fileInfo.fileSize; + + final resultJson = await argsUpload.dioClient.post( + Uri.decodeFull(uploadUri.toString()), + options: Options(headers: headerParam), + data: fileInfo.readStream, + cancelToken: cancelToken, + onSendProgress: (progress, total) { + log('FileUploader::_handleUploadAttachmentAction():onSendProgress: [${argsUpload.uploadId.id}] = $progress'); + sendPort.send(UploadingAttachmentUploadState( + argsUpload.uploadId, + progress, + fileInfo.fileSize)); + }); + + if (cancelToken?.isCancelled == true) { + log('FileUploader::_handleUploadAttachmentAction(): cancelToken'); + return null; + } + + final decodeJson = resultJson is Map ? resultJson : jsonDecode(resultJson); + final uploadResponse = UploadResponse.fromJson(decodeJson); + log('FileUploader::_handleUploadAttachmentAction(): ${uploadResponse.toString()}'); + return uploadResponse.toAttachment(fileInfo.fileName); + } catch (e) { + log('FileUploader::_handleUploadAttachmentAction(): ERROR: $e'); + return null; + } + } + + Future _handleUploadAttachmentActionOnWeb( + UploadTaskId uploadId, + StreamController> onSendController, + FileInfo fileInfo, + Uri uploadUri, + {CancelToken? cancelToken} + ) async { + try { + final headerParam = _dioClient.getHeaders(); + headerParam[HttpHeaders.contentTypeHeader] = fileInfo.mimeType; + headerParam[HttpHeaders.contentLengthHeader] = fileInfo.fileSize; + + final resultJson = await _dioClient.post( + Uri.decodeFull(uploadUri.toString()), + options: Options(headers: headerParam), + data: fileInfo.readStream, + cancelToken: cancelToken, + onSendProgress: (progress, total) { + log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: [$uploadId] = $progress'); + onSendController.add(Right(UploadingAttachmentUploadState( + uploadId, progress, fileInfo.fileSize))); + } + ); + + if (cancelToken?.isCancelled == true) { + log('FileUploader::_handleUploadAttachmentAction(): cancelToken'); + return null; + } + + final decodeJson = resultJson is Map ? resultJson : jsonDecode(resultJson); + final uploadResponse = UploadResponse.fromJson(decodeJson); + log('FileUploader::_handleUploadAttachmentActionOnWeb(): ${uploadResponse.toString()}'); + return uploadResponse.toAttachment(fileInfo.fileName); + } catch (e) { + log('FileUploader::_handleUploadAttachmentActionOnWeb(): $e'); + return null; + } + } +} \ No newline at end of file diff --git a/lib/features/upload/domain/model/upload_attachment.dart b/lib/features/upload/domain/model/upload_attachment.dart new file mode 100644 index 000000000..425b512e4 --- /dev/null +++ b/lib/features/upload/domain/model/upload_attachment.dart @@ -0,0 +1,65 @@ + +import 'dart:async'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; +import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/features/upload/data/network/file_uploader.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:dio/dio.dart'; + +class UploadAttachment extends Equatable { + + final UploadTaskId uploadTaskId; + final FileInfo fileInfo; + final Uri uploadUri; + final FileUploader fileUploader; + final CancelToken? cancelToken; + + final StreamController> _progressStateController + = StreamController>.broadcast(); + Stream> get progressState => _progressStateController.stream; + + UploadAttachment( + this.uploadTaskId, + this.fileInfo, + this.uploadUri, + this.fileUploader, + {this.cancelToken}); + + void _updateEvent(Either flowUploadState) { + _progressStateController.add(flowUploadState); + } + + Future upload() async { + log('UploadFile::upload(): $uploadTaskId'); + _updateEvent(Right(PendingAttachmentUploadState(uploadTaskId, 0, fileInfo.fileSize))); + + final attachment = await fileUploader.uploadAttachment( + uploadTaskId, + _progressStateController, + fileInfo, + uploadUri, + cancelToken: cancelToken); + + if (cancelToken?.isCancelled == true) { + _updateEvent(Left(CancelAttachmentUploadState(uploadTaskId))); + await _progressStateController.close(); + return; + } + + if (attachment != null) { + _updateEvent(Right(SuccessAttachmentUploadState(uploadTaskId, attachment))); + } else { + _updateEvent(Left(ErrorAttachmentUploadState(uploadTaskId))); + } + await _progressStateController.close(); + } + + @override + List get props => [uploadTaskId, fileInfo, uploadUri]; +} \ No newline at end of file diff --git a/lib/features/upload/domain/model/upload_task_id.dart b/lib/features/upload/domain/model/upload_task_id.dart new file mode 100644 index 000000000..bfaa74c30 --- /dev/null +++ b/lib/features/upload/domain/model/upload_task_id.dart @@ -0,0 +1,13 @@ + +import 'package:equatable/equatable.dart'; + +class UploadTaskId extends Equatable { + final String id; + + const UploadTaskId(this.id); + + factory UploadTaskId.undefined() => const UploadTaskId(''); + + @override + List get props => [id]; +} \ No newline at end of file diff --git a/lib/features/upload/domain/state/attachment_upload_state.dart b/lib/features/upload/domain/state/attachment_upload_state.dart new file mode 100644 index 000000000..f842b47b8 --- /dev/null +++ b/lib/features/upload/domain/state/attachment_upload_state.dart @@ -0,0 +1,55 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; + +class PendingAttachmentUploadState extends Success { + final UploadTaskId uploadId; + final int progress; + final int total; + + PendingAttachmentUploadState(this.uploadId, this.progress, this.total); + + @override + List get props => [progress, total]; +} + +class UploadingAttachmentUploadState extends Success { + final UploadTaskId uploadId; + final int progress; + final int total; + + UploadingAttachmentUploadState(this.uploadId, this.progress, this.total); + + @override + List get props => [progress, total]; +} + +class SuccessAttachmentUploadState extends Success { + final UploadTaskId uploadId; + final Attachment attachment; + + SuccessAttachmentUploadState(this.uploadId, this.attachment); + + @override + List get props => [uploadId, attachment]; +} + +class ErrorAttachmentUploadState extends Failure { + final UploadTaskId uploadId; + + ErrorAttachmentUploadState(this.uploadId); + + @override + List get props => [uploadId]; +} + +class CancelAttachmentUploadState extends Failure { + final UploadTaskId uploadId; + + CancelAttachmentUploadState(this.uploadId); + + @override + List get props => [uploadId]; +} \ No newline at end of file