using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Logging; using System.IO; namespace MotoresArgentinosV2.Infrastructure.Services; public interface IImageStorageService { Task SaveAdImageAsync(int adId, IFormFile file); void DeleteAdImage(string relativePath); } public class ImageStorageService : IImageStorageService { private readonly IWebHostEnvironment _env; private readonly ILogger _logger; public ImageStorageService(IWebHostEnvironment env, ILogger logger) { _env = env; _logger = logger; } // Firmas de archivos (Magic Numbers) para JPG, PNG, WEBP private static readonly Dictionary> _fileSignatures = new Dictionary> { { ".jpeg", new List { new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 } } }, { ".jpg", new List { new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 } } }, { ".png", new List { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } }, { ".webp", new List { new byte[] { 0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50 } } } }; public async Task SaveAdImageAsync(int adId, IFormFile file) { // 1. Validación de Extensión Básica var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); if (string.IsNullOrEmpty(ext) || !_fileSignatures.ContainsKey(ext)) { throw new Exception("Formato de archivo no permitido. Solo JPG, PNG y WEBP."); } // 2. Validación de Tamaño (Max 3MB) if (file.Length > 3 * 1024 * 1024) { throw new Exception("El archivo excede los 3MB permitidos."); } // 3. Validación de Magic Numbers (Leer cabecera real) using (var stream = file.OpenReadStream()) using (var reader = new BinaryReader(stream)) { // Leemos hasta 12 bytes que cubren nuestras firmas soportadas var headerBytes = reader.ReadBytes(12); bool isRealImage = false; // A. JPG (Flexible: FF D8) if (headerBytes.Length >= 2 && headerBytes[0] == 0xFF && headerBytes[1] == 0xD8) { isRealImage = true; } // B. PNG (Sello estricto) else if (headerBytes.Length >= 8 && headerBytes.Take(8).SequenceEqual(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A })) { isRealImage = true; } // C. WEBP (RIFF [4 bytes] WEBP) else if (headerBytes.Length >= 12 && headerBytes.Take(4).SequenceEqual(new byte[] { 0x52, 0x49, 0x46, 0x46 }) && headerBytes.Skip(8).Take(4).SequenceEqual(new byte[] { 0x57, 0x45, 0x42, 0x50 })) { isRealImage = true; } if (!isRealImage) { string hex = BitConverter.ToString(headerBytes.Take(8).ToArray()); _logger.LogWarning("Firma de archivo inválida para {Extension}: {HexBytes}", ext, hex); throw new Exception($"El archivo parece corrupto o tiene una firma inválida ({hex}). El sistema acepta JPG, PNG y WEBP reales."); } } try { // 1. Definir rutas var uploadFolder = Path.Combine(_env.WebRootPath, "uploads", "ads", adId.ToString()); if (!Directory.Exists(uploadFolder)) Directory.CreateDirectory(uploadFolder); var uniqueName = Guid.NewGuid().ToString(); var fileName = $"{uniqueName}.jpg"; var thumbName = $"{uniqueName}_thumb.jpg"; var filePath = Path.Combine(uploadFolder, fileName); var thumbPath = Path.Combine(uploadFolder, thumbName); // 2. Cargar y Procesar con ImageSharp using (var image = await Image.LoadAsync(file.OpenReadStream())) { // A. Guardar imagen principal (Optimized: Max width 1280px) if (image.Width > 1280) { image.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(1280, 0), // 0 mantiene aspect ratio Mode = ResizeMode.Max })); } await image.SaveAsJpegAsync(filePath); // B. Generar Thumbnail (Max width 400px para grillas) image.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(400, 300), Mode = ResizeMode.Crop // Recorte inteligente para que queden parejitas })); await image.SaveAsJpegAsync(thumbPath); } // Retornar ruta relativa web de la imagen principal return $"/uploads/ads/{adId}/{fileName}"; } catch (Exception ex) { _logger.LogError(ex, "Error procesando imagen para el aviso {AdId}", adId); throw; } } public void DeleteAdImage(string relativePath) { if (string.IsNullOrEmpty(relativePath)) return; try { // Eliminar archivo principal var fullPath = Path.Combine(_env.WebRootPath, relativePath.TrimStart('/').Replace('/', Path.DirectorySeparatorChar)); if (File.Exists(fullPath)) File.Delete(fullPath); // Eliminar thumbnail asociado var thumbPath = fullPath.Replace(".jpg", "_thumb.jpg"); if (File.Exists(thumbPath)) File.Delete(thumbPath); } catch (Exception ex) { _logger.LogError(ex, "Error eliminando imagen {Path}", relativePath); } } }