diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs index 245d8b39..186f47b2 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs @@ -28,26 +28,25 @@ public class SendMessageCommandHandler : IBaseCommandHandler> Handle(SendMessageCommand request, CancellationToken cancellationToken) + public async Task> Handle(SendMessageCommand request, + CancellationToken cancellationToken) { var currentUserId = _authHelper.GetCurrentUserId() ?? throw new UnAuthorizedException("کاربر احراز هویت نشده است"); @@ -57,75 +56,21 @@ public class SendMessageCommandHandler : IBaseCommandHandler.NotFound("تسک یافت نشد"); } - + Guid? uploadedFileId = null; if (request.File != null) { - if (request.File.Length == 0) - { - return OperationResult.ValidationError("فایل خالی است"); - } - - const long maxFileSize = 100 * 1024 * 1024; - if (request.File.Length > maxFileSize) - { - return OperationResult.ValidationError("حجم فایل بیش از حد مجاز است (حداکثر 100MB)"); - } - - var fileType = DetectFileType(request.File.ContentType, Path.GetExtension(request.File.FileName)); - - var uploadedFile = new UploadedFile( - originalFileName: request.File.FileName, - fileSizeBytes: request.File.Length, - mimeType: request.File.ContentType, - fileType: fileType, - category: FileCategory.TaskChatMessage, - uploadedByUserId: currentUserId, - storageProvider: StorageProvider.LocalFileSystem + var uploadedFile = await _fileUploadService.UploadFileAsync + ( + request.File, + FileCategory.TaskChatMessage, + currentUserId ); - - await _fileRepository.AddAsync(uploadedFile); - await _fileRepository.SaveChangesAsync(); - - try + if (!uploadedFile.IsSuccess) { - using var stream = request.File.OpenReadStream(); - var uploadResult = await _fileStorageService.UploadAsync( - stream, - uploadedFile.UniqueFileName, - "TaskChatMessage" - ); - - uploadedFile.CompleteUpload(uploadResult.StoragePath, uploadResult.StorageUrl); - - if (fileType == FileType.Image) - { - var dimensions = await _thumbnailService.GetImageDimensionsAsync(uploadResult.StoragePath); - if (dimensions.HasValue) - { - uploadedFile.SetImageDimensions(dimensions.Value.Width, dimensions.Value.Height); - } - - var thumbnail = await _thumbnailService - .GenerateImageThumbnailAsync(uploadResult.StoragePath, category: "TaskChatMessage"); - if (thumbnail.HasValue) - { - uploadedFile.SetThumbnail(thumbnail.Value.ThumbnailUrl); - } - } - - await _fileRepository.UpdateAsync(uploadedFile); - await _fileRepository.SaveChangesAsync(); - - uploadedFileId = uploadedFile.Id; - } - catch (Exception ex) - { - await _fileRepository.DeleteAsync(uploadedFile); - await _fileRepository.SaveChangesAsync(); - - return OperationResult.ValidationError($"خطا در آپلود فایل: {ex.Message}"); + return OperationResult.Failure(uploadedFile.ErrorMessage ?? "خطا در آپلود فایل"); } + uploadedFileId = uploadedFile.FileId!.Value; } var message = new TaskChatMessage( @@ -209,4 +154,4 @@ public class SendMessageCommandHandler : IBaseCommandHandler +/// سرویس آپلود و مدیریت کامل فایل +/// این سرویس تمام مراحل آپلود، ذخیره، تولید thumbnail و... را انجام می‌دهد +/// +public interface IFileUploadService +{ + /// + /// آپلود فایل با تمام مراحل پردازش + /// + /// فایل برای آپلود + /// دسته‌بندی فایل + /// شناسه کاربر آپلودکننده + /// حداکثر حجم مجاز فایل (پیش‌فرض: 100MB) + /// شناسه فایل آپلود شده یا null در صورت خطا + Task UploadFileAsync( + IFormFile file, + FileCategory category, + long uploadedByUserId, + long maxFileSizeBytes = 100 * 1024 * 1024); + + /// + /// آپلود فایل با Stream + /// + Task UploadFileFromStreamAsync( + Stream fileStream, + string fileName, + string contentType, + FileCategory category, + long uploadedByUserId, + long maxFileSizeBytes = 100 * 1024 * 1024); +} + +/// +/// نتیجه عملیات آپلود فایل +/// +public class FileUploadResult +{ + public bool IsSuccess { get; set; } + public Guid? FileId { get; set; } + public string? ErrorMessage { get; set; } + public string? StorageUrl { get; set; } + public string? ThumbnailUrl { get; set; } + + public static FileUploadResult Success(Guid fileId, string storageUrl, string? thumbnailUrl = null) + { + return new FileUploadResult + { + IsSuccess = true, + FileId = fileId, + StorageUrl = storageUrl, + ThumbnailUrl = thumbnailUrl + }; + } + + public static FileUploadResult Failed(string errorMessage) + { + return new FileUploadResult + { + IsSuccess = false, + ErrorMessage = errorMessage + }; + } +} + diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs index 20618330..cf9ec45e 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs @@ -92,6 +92,7 @@ public static class DependencyInjection // File Storage Services services.AddScoped(); services.AddScoped(); + services.AddScoped(); // JWT Settings services.Configure(configuration.GetSection("JwtSettings")); diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/FileUploadService.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/FileUploadService.cs new file mode 100644 index 00000000..ec50de7e --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/FileUploadService.cs @@ -0,0 +1,247 @@ +using GozareshgirProgramManager.Application.Services.FileManagement; +using GozareshgirProgramManager.Domain.FileManagementAgg.Entities; +using GozareshgirProgramManager.Domain.FileManagementAgg.Enums; +using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement; + +/// +/// پیاده‌سازی سرویس آپلود کامل فایل +/// +public class FileUploadService : IFileUploadService +{ + private readonly IUploadedFileRepository _fileRepository; + private readonly IFileStorageService _fileStorageService; + private readonly IThumbnailGeneratorService _thumbnailService; + private readonly ILogger _logger; + + public FileUploadService( + IUploadedFileRepository fileRepository, + IFileStorageService fileStorageService, + IThumbnailGeneratorService thumbnailService, + ILogger logger) + { + _fileRepository = fileRepository; + _fileStorageService = fileStorageService; + _thumbnailService = thumbnailService; + _logger = logger; + } + + public async Task UploadFileAsync( + IFormFile file, + FileCategory category, + long uploadedByUserId, + long maxFileSizeBytes = 100 * 1024 * 1024) + { + try + { + // اعتبارسنجی ورودی + if (file.Length == 0) + { + return FileUploadResult.Failed("فایل خالی است"); + } + + if (file.Length > maxFileSizeBytes) + { + var maxSizeMb = maxFileSizeBytes / (1024 * 1024); + return FileUploadResult.Failed($"حجم فایل بیش از حد مجاز است (حداکثر {maxSizeMb}MB)"); + } + + using var stream = file.OpenReadStream(); + return await UploadFileFromStreamAsync( + stream, + file.FileName, + file.ContentType, + category, + uploadedByUserId, + maxFileSizeBytes); + } + catch (Exception ex) + { + _logger.LogError(ex, "خطا در آپلود فایل {FileName}", file.FileName); + return FileUploadResult.Failed($"خطا در آپلود فایل: {ex.Message}"); + } + } + + public async Task UploadFileFromStreamAsync( + Stream fileStream, + string fileName, + string contentType, + FileCategory category, + long uploadedByUserId, + long maxFileSizeBytes = 100 * 1024 * 1024) + { + UploadedFile? uploadedFile = null; + + try + { + // تشخیص نوع فایل + var fileType = DetectFileType(contentType, Path.GetExtension(fileName)); + + // ایجاد رکورد فایل در دیتابیس + uploadedFile = new UploadedFile( + originalFileName: fileName, + fileSizeBytes: fileStream.Length, + mimeType: contentType, + fileType: fileType, + category: category, + uploadedByUserId: uploadedByUserId, + storageProvider: StorageProvider.LocalFileSystem + ); + + await _fileRepository.AddAsync(uploadedFile); + await _fileRepository.SaveChangesAsync(); + + // آپلود فایل به استوریج + var categoryFolder = GetCategoryFolderName(category); + var uploadResult = await _fileStorageService.UploadAsync( + fileStream, + uploadedFile.UniqueFileName, + categoryFolder + ); + + // به‌روزرسانی اطلاعات آپلود + uploadedFile.CompleteUpload(uploadResult.StoragePath, uploadResult.StorageUrl); + + // پردازش‌های خاص بر اساس نوع فایل + string? thumbnailUrl = null; + if (fileType == FileType.Image) + { + thumbnailUrl = await ProcessImageAsync(uploadedFile, uploadResult.StoragePath, categoryFolder); + } + else if (fileType == FileType.Video) + { + thumbnailUrl = await ProcessVideoAsync(uploadedFile, uploadResult.StoragePath, categoryFolder); + } + + // ذخیره تغییرات نهایی + await _fileRepository.UpdateAsync(uploadedFile); + await _fileRepository.SaveChangesAsync(); + + _logger.LogInformation( + "فایل {FileName} با شناسه {FileId} با موفقیت آپلود شد", + fileName, + uploadedFile.Id); + + return FileUploadResult.Success(uploadedFile.Id, uploadResult.StorageUrl, thumbnailUrl); + } + catch (Exception ex) + { + _logger.LogError(ex, "خطا در آپلود فایل {FileName}", fileName); + + // در صورت خطا، فایل آپلود شده را حذف کنیم + if (uploadedFile != null) + { + try + { + await _fileRepository.DeleteAsync(uploadedFile); + await _fileRepository.SaveChangesAsync(); + } + catch (Exception deleteEx) + { + _logger.LogError(deleteEx, "خطا در حذف فایل ناموفق {FileId}", uploadedFile.Id); + } + } + + return FileUploadResult.Failed($"خطا در آپلود فایل: {ex.Message}"); + } + } + + /// + /// پردازش تصویر (ابعاد و thumbnail) + /// + private async Task ProcessImageAsync(UploadedFile file, string storagePath, string categoryFolder) + { + try + { + // دریافت ابعاد تصویر + var dimensions = await _thumbnailService.GetImageDimensionsAsync(storagePath); + if (dimensions.HasValue) + { + file.SetImageDimensions(dimensions.Value.Width, dimensions.Value.Height); + } + + // تولید thumbnail + var thumbnail = await _thumbnailService.GenerateImageThumbnailAsync( + storagePath, + category: categoryFolder); + + if (thumbnail.HasValue) + { + file.SetThumbnail(thumbnail.Value.ThumbnailUrl); + return thumbnail.Value.ThumbnailUrl; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "خطا در پردازش تصویر {FileId}", file.Id); + } + + return null; + } + + /// + /// پردازش ویدیو (thumbnail) + /// + private async Task ProcessVideoAsync(UploadedFile file, string storagePath, string categoryFolder) + { + try + { + // تولید thumbnail از ویدیو + var thumbnail = await _thumbnailService.GenerateVideoThumbnailAsync( + storagePath, + category: categoryFolder); + + if (thumbnail.HasValue) + { + file.SetThumbnail(thumbnail.Value.ThumbnailUrl); + return thumbnail.Value.ThumbnailUrl; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "خطا در پردازش ویدیو {FileId}", file.Id); + } + + return null; + } + + /// + /// تشخیص نوع فایل از روی MIME type و extension + /// + private FileType DetectFileType(string mimeType, string extension) + { + if (mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + return FileType.Image; + + if (mimeType.StartsWith("video/", StringComparison.OrdinalIgnoreCase)) + return FileType.Video; + + if (mimeType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase)) + return FileType.Audio; + + if (new[] { ".zip", ".rar", ".7z", ".tar", ".gz" }.Contains(extension.ToLower())) + return FileType.Archive; + + return FileType.Document; + } + + /// + /// دریافت نام پوشه بر اساس دسته‌بندی + /// + private string GetCategoryFolderName(FileCategory category) + { + return category switch + { + FileCategory.TaskChatMessage => "TaskChatMessage", + FileCategory.TaskAttachment => "TaskAttachment", + FileCategory.ProjectDocument => "ProjectDocument", + FileCategory.UserProfilePhoto => "UserProfilePhoto", + FileCategory.Report => "Report", + _ => "Other" + }; + } +} +