BCards/src/BCards.Web/Controllers/ImageController.cs
Ricardo Carneiro d700bd35a9
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 4s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 7m58s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m39s
BCards Deployment Pipeline / Deploy to Staging (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 1s
fix: ajustes de notificações e restrição de tamanho da imagem
2025-09-05 17:49:26 -03:00

202 lines
6.9 KiB
C#

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<ImageController> _logger;
public ImageController(IImageStorageService imageStorage, ILogger<ImageController> logger)
{
_imageStorage = imageStorage;
_logger = logger;
}
[HttpGet("{imageId}")]
[ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new[] { "imageId" })]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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;
}
}