Files
MotoresArgentinosV2/Backend/MotoresArgentinosV2.Infrastructure/Services/ImageStorageService.cs

149 lines
5.3 KiB
C#
Raw Normal View History

2026-01-29 13:43:44 -03:00
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<string> SaveAdImageAsync(int adId, IFormFile file);
void DeleteAdImage(string relativePath);
}
public class ImageStorageService : IImageStorageService
{
private readonly IWebHostEnvironment _env;
private readonly ILogger<ImageStorageService> _logger;
public ImageStorageService(IWebHostEnvironment env, ILogger<ImageStorageService> logger)
{
_env = env;
_logger = logger;
}
// Firmas de archivos (Magic Numbers) para JPG, PNG, WEBP
private static readonly Dictionary<string, List<byte[]>> _fileSignatures = new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]> { new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 } } },
{ ".jpg", new List<byte[]> { new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 } } },
{ ".png", new List<byte[]> { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } },
{ ".webp", new List<byte[]> { new byte[] { 0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50 } } }
};
public async Task<string> 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);
}
}
}