using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Protobuf.Protos.FMS; using Google.Protobuf; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Processing; using System.IO; namespace CMSMicroservice.Infrastructure.Services; public class FileManagementService : IFileManagementService, IDisposable { private readonly ILogger _logger; private readonly FileInfoContract.FileInfoContractClient _client; private readonly GrpcChannel _channel; private const int MainImageMaxWidth = 1200; private const int MainImageMaxHeight = 1200; private const int ThumbnailMaxWidth = 300; private const int ThumbnailMaxHeight = 300; private const int JpegQuality = 75; public FileManagementService(IConfiguration configuration, ILogger logger) { _logger = logger; var fmsAddress = configuration["FMS:Address"] ?? "https://dl.afrino.co"; _channel = GrpcChannel.ForAddress(fmsAddress, new GrpcChannelOptions { MaxReceiveMessageSize = 100 * 1024 * 1024, // 100 MB MaxSendMessageSize = 100 * 1024 * 1024 }); _client = new FileInfoContract.FileInfoContractClient(_channel); } public async Task UploadFileAsync(string directory, byte[] fileBytes, string mime, string? fileName, CancellationToken cancellationToken = default) { try { var request = new CreateNewFileInfoRequest { Directory = directory, File = ByteString.CopyFrom(fileBytes), Mime = mime, IsBase64 = false }; if (!string.IsNullOrWhiteSpace(fileName)) request.FileName = fileName; var response = await _client.CreateNewFileInfoAsync(request, cancellationToken: cancellationToken); if (response != null && !string.IsNullOrWhiteSpace(response.File)) { _logger.LogInformation("File uploaded to FMS successfully. Id: {Id}, Path: {Path}", response.Id, response.File); return response.File; } _logger.LogWarning("FMS upload returned null or empty path for file: {FileName}", fileName); return null; } catch (Exception ex) { _logger.LogError(ex, "Error uploading file to FMS. Directory: {Directory}, FileName: {FileName}", directory, fileName); return null; } } public async Task<(string? MainImagePath, string? ThumbnailPath)> UploadImageWithThumbnailAsync( string directory, byte[] fileBytes, string mime, string? fileName, CancellationToken cancellationToken = default) { string? mainImagePath = null; string? thumbnailPath = null; try { // Optimize main image var mainImageBytes = await OptimizeImageAsync(fileBytes, MainImageMaxWidth, MainImageMaxHeight); mainImagePath = await UploadFileAsync(directory, mainImageBytes, "image/jpeg", fileName, cancellationToken); // Create and upload thumbnail var thumbnailBytes = await OptimizeImageAsync(fileBytes, ThumbnailMaxWidth, ThumbnailMaxHeight); var thumbFileName = fileName != null ? $"thumb_{fileName}" : null; thumbnailPath = await UploadFileAsync($"{directory}/Thumbnails", thumbnailBytes, "image/jpeg", thumbFileName, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Error processing and uploading image with thumbnail. Directory: {Directory}", directory); } return (mainImagePath, thumbnailPath); } public async Task DeleteFileAsync(long fileId, CancellationToken cancellationToken = default) { try { var request = new DeleteFileInfoRequest { Id = fileId }; var response = await _client.DeleteFileInfoAsync(request, cancellationToken: cancellationToken); return response?.Success ?? false; } catch (Exception ex) { _logger.LogError(ex, "Error deleting file from FMS. FileId: {FileId}", fileId); return false; } } private static async Task OptimizeImageAsync(byte[] imageBytes, int maxWidth, int maxHeight) { using var image = Image.Load(imageBytes); // Only resize if larger than max dimensions if (image.Width > maxWidth || image.Height > maxHeight) { image.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(maxWidth, maxHeight), Mode = ResizeMode.Max })); } using var ms = new MemoryStream(); await image.SaveAsJpegAsync(ms, new JpegEncoder { Quality = JpegQuality }); return ms.ToArray(); } public void Dispose() { _channel?.Dispose(); } }