checking stuff
This commit is contained in:
154
src/infrastructure/adapters/file_storage.py
Normal file
154
src/infrastructure/adapters/file_storage.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
"""File storage utilities for report images"""
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import UploadFile, HTTPException, status
|
||||||
|
from PIL import Image
|
||||||
|
from io import BytesIO
|
||||||
|
from core.config import ConfSettings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageStorageManager:
|
||||||
|
"""Maneja almacenamiento, compresión y eliminación de imágenes de reportes"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.storage_path = Path(ConfSettings.storage_base_path) / ConfSettings.images_dir
|
||||||
|
self.max_size_bytes = ConfSettings.images_max_size_mb * 1024 * 1024
|
||||||
|
self.allowed_types = ConfSettings.images_allowed_types
|
||||||
|
self.compression_quality = ConfSettings.images_compression_quality
|
||||||
|
|
||||||
|
# Crear directorio si no existe
|
||||||
|
self.storage_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
logger.info(f"ImageStorageManager initialized with path: {self.storage_path}")
|
||||||
|
|
||||||
|
def validate_and_save_image(self, file: UploadFile, report_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Valida y guarda una imagen, comprimiendo a WebP.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file: Archivo subido (UploadFile)
|
||||||
|
report_id: ID del reporte para nombrado del archivo
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Nombre del archivo guardado (sin ruta)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: Si hay error en validación o guardado
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validar tipo MIME
|
||||||
|
if file.content_type not in self.allowed_types:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"Tipo de archivo no permitido. Permitidos: {', '.join(self.allowed_types)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Leer contenido
|
||||||
|
content = file.file.read()
|
||||||
|
|
||||||
|
# Validar tamaño
|
||||||
|
if len(content) > self.max_size_bytes:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||||
|
detail=f"Archivo demasiado grande. Máximo: {ConfSettings.images_max_size_mb}MB"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Abrir imagen con Pillow
|
||||||
|
try:
|
||||||
|
image = Image.open(BytesIO(content))
|
||||||
|
image.verify() # Verificar que sea una imagen válida
|
||||||
|
|
||||||
|
# Reabrir después de verify() que la cierra
|
||||||
|
image = Image.open(BytesIO(content))
|
||||||
|
|
||||||
|
# Convertir a RGB si tiene canal alpha (RGBA)
|
||||||
|
if image.mode in ('RGBA', 'LA', 'P'):
|
||||||
|
rgb_image = Image.new('RGB', image.size, (255, 255, 255))
|
||||||
|
rgb_image.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
|
||||||
|
image = rgb_image
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error validating image: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Archivo no es una imagen válida"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Guardar como WebP comprimido
|
||||||
|
filename = f"{report_id}.webp"
|
||||||
|
filepath = self.storage_path / filename
|
||||||
|
|
||||||
|
try:
|
||||||
|
image.save(
|
||||||
|
filepath,
|
||||||
|
"WEBP",
|
||||||
|
quality=self.compression_quality,
|
||||||
|
method=6
|
||||||
|
)
|
||||||
|
logger.info(f"Image saved: {filename} ({len(open(filepath, 'rb').read())} bytes)")
|
||||||
|
return filename
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error saving image to disk: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Error al guardar la imagen"
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error processing image: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Error procesando la imagen"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_image_path(self, filename: str) -> Path:
|
||||||
|
"""Obtiene la ruta completa de una imagen"""
|
||||||
|
return self.storage_path / filename
|
||||||
|
|
||||||
|
def image_exists(self, filename: str) -> bool:
|
||||||
|
"""Verifica si una imagen existe"""
|
||||||
|
if not filename:
|
||||||
|
return False
|
||||||
|
filepath = self.get_image_path(filename)
|
||||||
|
return filepath.exists()
|
||||||
|
|
||||||
|
def delete_image(self, filename: str) -> bool:
|
||||||
|
"""
|
||||||
|
Elimina una imagen del almacenamiento.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Nombre del archivo a eliminar
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True si se eliminó, False si no existía
|
||||||
|
"""
|
||||||
|
if not filename:
|
||||||
|
return False
|
||||||
|
|
||||||
|
filepath = self.get_image_path(filename)
|
||||||
|
try:
|
||||||
|
if filepath.exists():
|
||||||
|
filepath.unlink() # Eliminar archivo
|
||||||
|
logger.info(f"Image deleted: {filename}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"Image not found for deletion: {filename}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting image {filename}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_image_url(self, filename: str) -> str:
|
||||||
|
"""Genera la URL pública para una imagen"""
|
||||||
|
if not filename:
|
||||||
|
return None
|
||||||
|
return f"/images/{filename}"
|
||||||
|
|
||||||
|
|
||||||
|
# Instancia global
|
||||||
|
image_storage = ImageStorageManager()
|
||||||
Reference in New Issue
Block a user