149 lines
5.3 KiB
C#
149 lines
5.3 KiB
C#
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|