using BCards.Web.Services; using Microsoft.AspNetCore.Mvc; namespace BCards.Web.Controllers; [Route("api/[controller]")] [ApiController] public class ImageController : ControllerBase { private readonly IImageStorageService _imageStorage; private readonly ILogger _logger; public ImageController(IImageStorageService imageStorage, ILogger logger) { _imageStorage = imageStorage; _logger = logger; } [HttpGet("{imageId}")] [ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new[] { "imageId" })] public async Task GetImage(string imageId) { try { if (string.IsNullOrEmpty(imageId)) { _logger.LogWarning("Image request with empty ID"); return BadRequest("Image ID is required"); } var imageBytes = await _imageStorage.GetImageAsync(imageId); if (imageBytes == null || imageBytes.Length == 0) { _logger.LogWarning("Image not found: {ImageId}", imageId); return NotFound("Image not found"); } // Headers de cache mais agressivos para imagens Response.Headers["Cache-Control"] = "public, max-age=31536000"; // 1 ano Response.Headers["Expires"] = DateTime.UtcNow.AddYears(1).ToString("R"); Response.Headers["ETag"] = $"\"{imageId}\""; return File(imageBytes, "image/jpeg", enableRangeProcessing: true); } catch (Exception ex) { _logger.LogError(ex, "Error retrieving image: {ImageId}", imageId); return NotFound("Image not found"); } } [HttpPost("upload")] [RequestSizeLimit(2 * 1024 * 1024)] // 2MB máximo - otimizado para celulares [DisableRequestSizeLimit] // Para formulários grandes public async Task UploadImage(IFormFile file) { try { if (file == null || file.Length == 0) { _logger.LogWarning("Upload request with no file"); return BadRequest(new { error = "No file provided", code = "NO_FILE" }); } // Validações de tipo var allowedTypes = new[] { "image/jpeg", "image/jpg", "image/png", "image/gif" }; if (!allowedTypes.Contains(file.ContentType.ToLower())) { _logger.LogWarning("Invalid file type uploaded: {ContentType}", file.ContentType); return BadRequest(new { error = "Invalid file type. Only JPEG, PNG and GIF are allowed.", code = "INVALID_TYPE" }); } // Validação de tamanho if (file.Length > 2 * 1024 * 1024) // 2MB { _logger.LogWarning("File too large: {Size}MB", file.Length / (1024 * 1024)); return BadRequest(new { error = "Arquivo muito grande. Tamanho máximo: 2MB.", code = "FILE_TOO_LARGE" }); } // Processar upload using var memoryStream = new MemoryStream(); await file.CopyToAsync(memoryStream); var imageBytes = memoryStream.ToArray(); // Validação adicional: verificar se é realmente uma imagem if (!IsValidImageBytes(imageBytes)) { _logger.LogWarning("Invalid image data uploaded"); return BadRequest(new { error = "Invalid image data.", code = "INVALID_IMAGE" }); } var imageId = await _imageStorage.SaveImageAsync(imageBytes, file.FileName, file.ContentType); _logger.LogInformation("Image uploaded successfully: {ImageId}, Original: {FileName}, Size: {Size}KB", imageId, file.FileName, file.Length / 1024); return Ok(new { success = true, imageId, url = $"/api/image/{imageId}", originalSize = file.Length, fileName = file.FileName }); } catch (ArgumentException ex) { _logger.LogWarning(ex, "Invalid upload parameters"); return BadRequest(new { error = ex.Message, code = "VALIDATION_ERROR" }); } catch (Exception ex) { _logger.LogError(ex, "Error uploading image: {FileName}", file?.FileName); return StatusCode(500, new { error = "Error uploading image. Please try again.", code = "UPLOAD_ERROR" }); } } [HttpDelete("{imageId}")] public async Task DeleteImage(string imageId) { try { if (string.IsNullOrEmpty(imageId)) return BadRequest(new { error = "Image ID is required" }); var deleted = await _imageStorage.DeleteImageAsync(imageId); if (!deleted) return NotFound(new { error = "Image not found" }); _logger.LogInformation("Image deleted: {ImageId}", imageId); return Ok(new { success = true, message = "Image deleted successfully" }); } catch (Exception ex) { _logger.LogError(ex, "Error deleting image: {ImageId}", imageId); return StatusCode(500, new { error = "Error deleting image" }); } } [HttpHead("{imageId}")] public async Task ImageExists(string imageId) { try { if (string.IsNullOrEmpty(imageId)) return BadRequest(); var exists = await _imageStorage.ImageExistsAsync(imageId); return exists ? Ok() : NotFound(); } catch (Exception ex) { _logger.LogError(ex, "Error checking image existence: {ImageId}", imageId); return StatusCode(500); } } private static bool IsValidImageBytes(byte[] bytes) { if (bytes == null || bytes.Length < 4) return false; // Verificar assinaturas de arquivos de imagem var jpegSignature = new byte[] { 0xFF, 0xD8, 0xFF }; var pngSignature = new byte[] { 0x89, 0x50, 0x4E, 0x47 }; var gifSignature = new byte[] { 0x47, 0x49, 0x46 }; return StartsWithSignature(bytes, jpegSignature) || StartsWithSignature(bytes, pngSignature) || StartsWithSignature(bytes, gifSignature); } private static bool StartsWithSignature(byte[] bytes, byte[] signature) { if (bytes.Length < signature.Length) return false; for (int i = 0; i < signature.Length; i++) { if (bytes[i] != signature[i]) return false; } return true; } }