vista de pdf

This commit is contained in:
Sebastian
2025-10-29 07:50:50 +00:00
parent 46c07568bc
commit df2c184814
13 changed files with 795 additions and 102 deletions

View File

@@ -19,4 +19,5 @@ RUN uv sync --frozen
EXPOSE 8000
# Comando para desarrollo (con reload)
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
# Reinstalar dependencias al inicio para asegurar compatibilidad con volumen montado
CMD ["sh", "-c", "uv sync --frozen && uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"]

View File

@@ -582,7 +582,7 @@ async def get_file_info(
blob_name=filename,
tema=tema or ""
)
# Convertir a objeto FileInfo
file_info = FileInfo(
name=file_data["name"],
@@ -593,12 +593,63 @@ async def get_file_info(
content_type=file_data.get("content_type"),
url=file_data.get("url")
)
logger.info(f"Información obtenida para archivo '{filename}'")
return file_info
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"Archivo '{filename}' no encontrado")
except Exception as e:
logger.error(f"Error obteniendo info del archivo '{filename}': {e}")
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
@router.get("/{filename}/preview-url")
async def get_file_preview_url(
filename: str,
tema: Optional[str] = Query(None, description="Tema donde está el archivo"),
expiry_hours: int = Query(1, description="Horas de validez de la URL (máximo 24)", ge=1, le=24)
):
"""
Generar una URL temporal (SAS) para vista previa de archivos
Este endpoint genera una URL con firma temporal (Shared Access Signature)
que permite acceder al archivo directamente desde el navegador sin autenticación.
La URL expira después del tiempo especificado por seguridad.
Casos de uso:
- Vista previa de PDFs en el navegador
- Mostrar imágenes sin descargarlas
- Compartir acceso temporal a archivos
Args:
filename: Nombre del archivo
tema: Tema donde está ubicado el archivo (opcional)
expiry_hours: Horas de validez de la URL (1-24 horas, por defecto 1)
Returns:
JSON con la URL temporal del archivo
"""
try:
# Generar SAS URL usando el servicio de Azure
sas_url = await azure_service.generate_sas_url(
blob_name=filename,
tema=tema or "",
expiry_hours=expiry_hours
)
logger.info(f"SAS URL generada para preview de '{filename}'" + (f" del tema '{tema}'" if tema else ""))
return {
"success": True,
"filename": filename,
"url": sas_url,
"expiry_hours": expiry_hours,
"message": f"URL temporal generada (válida por {expiry_hours} hora{'s' if expiry_hours > 1 else ''})"
}
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"Archivo '{filename}' no encontrado")
except Exception as e:
logger.error(f"Error generando preview URL para '{filename}': {e}")
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")

View File

@@ -1,8 +1,8 @@
from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient
from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient, generate_blob_sas, BlobSasPermissions
from azure.core.exceptions import ResourceNotFoundError, ResourceExistsError
from typing import List, Optional, BinaryIO
import logging
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
import os
from ..core.config import settings
@@ -22,9 +22,40 @@ class AzureBlobService:
)
self.container_name = settings.AZURE_CONTAINER_NAME
logger.info(f"Cliente de Azure Blob Storage inicializado para container: {self.container_name}")
# Configurar CORS automáticamente al inicializar
self._configure_cors()
except Exception as e:
logger.error(f"Error inicializando Azure Blob Service: {e}")
raise e
def _configure_cors(self):
"""
Configurar CORS en Azure Blob Storage para permitir acceso desde el frontend
Esto es necesario para que el navegador pueda cargar PDFs directamente
desde Azure usando las URLs SAS generadas.
"""
try:
from azure.storage.blob import CorsRule
# Definir regla CORS permisiva para desarrollo y producción
cors_rule = CorsRule(
allowed_origins=["*"], # En producción, especificar dominios exactos
allowed_methods=["GET", "HEAD", "OPTIONS"],
allowed_headers=["*"],
exposed_headers=["*"],
max_age_in_seconds=3600
)
# Aplicar la configuración CORS
self.blob_service_client.set_service_properties(cors=[cors_rule])
logger.info("CORS configurado exitosamente en Azure Blob Storage")
except Exception as e:
# No fallar si CORS no se puede configurar (puede que ya esté configurado)
logger.warning(f"No se pudo configurar CORS automáticamente: {e}")
logger.warning("Asegúrate de configurar CORS manualmente en Azure Portal si es necesario")
async def create_container_if_not_exists(self) -> bool:
"""
@@ -237,30 +268,112 @@ class AzureBlobService:
async def get_download_url(self, blob_name: str, tema: str = "") -> str:
"""
Obtener URL de descarga directa para un archivo
Args:
blob_name: Nombre del archivo
tema: Tema/carpeta donde está el archivo
Returns:
str: URL de descarga
"""
try:
# Construir la ruta completa
full_blob_name = f"{tema}/{blob_name}" if tema else blob_name
# Obtener cliente del blob
blob_client = self.blob_service_client.get_blob_client(
container=self.container_name,
blob=full_blob_name
)
return blob_client.url
except Exception as e:
logger.error(f"Error obteniendo URL de descarga para '{blob_name}': {e}")
raise e
async def generate_sas_url(self, blob_name: str, tema: str = "", expiry_hours: int = 1) -> str:
"""
Generar una URL SAS (Shared Access Signature) temporal para acceder a un archivo
Esta URL permite acceso temporal y seguro al archivo sin requerir autenticación.
Es ideal para vistas previas de archivos en el navegador.
Args:
blob_name: Nombre del archivo
tema: Tema/carpeta donde está el archivo
expiry_hours: Horas de validez de la URL (por defecto 1 hora)
Returns:
str: URL completa con SAS token para acceso temporal
"""
try:
from azure.storage.blob import ContentSettings
# Construir la ruta completa del blob
full_blob_name = f"{tema}/{blob_name}" if tema else blob_name
# Obtener cliente del blob
blob_client = self.blob_service_client.get_blob_client(
container=self.container_name,
blob=full_blob_name
)
# Verificar que el archivo existe antes de generar el SAS
if not blob_client.exists():
raise FileNotFoundError(f"El archivo '{blob_name}' no existe")
# IMPORTANTE: Configurar el blob para que se muestre inline (no descarga)
# Esto hace que el navegador muestre el PDF en lugar de descargarlo
try:
content_settings = ContentSettings(
content_type='application/pdf',
content_disposition='inline' # Clave para mostrar en navegador
)
blob_client.set_http_headers(content_settings=content_settings)
logger.info(f"Headers configurados para visualización inline de '{full_blob_name}'")
except Exception as e:
logger.warning(f"No se pudieron configurar headers inline: {e}")
# Definir el tiempo de expiración del SAS token
start_time = datetime.now(timezone.utc)
expiry_time = start_time + timedelta(hours=expiry_hours)
# Extraer la account key del connection string para generar el SAS
# El SAS necesita la account key para firmar el token
account_key = None
for part in settings.AZURE_STORAGE_CONNECTION_STRING.split(';'):
if part.startswith('AccountKey='):
account_key = part.split('=', 1)[1]
break
if not account_key:
raise ValueError("No se pudo extraer AccountKey del connection string")
# Generar el SAS token con permisos de solo lectura
sas_token = generate_blob_sas(
account_name=blob_client.account_name,
container_name=self.container_name,
blob_name=full_blob_name,
account_key=account_key,
permission=BlobSasPermissions(read=True), # Solo permisos de lectura
expiry=expiry_time,
start=start_time
)
# Construir la URL completa con el SAS token
sas_url = f"{blob_client.url}?{sas_token}"
logger.info(f"SAS URL generada para '{full_blob_name}' (válida por {expiry_hours} horas)")
return sas_url
except FileNotFoundError:
logger.error(f"Archivo '{full_blob_name}' no encontrado para generar SAS")
raise
except Exception as e:
logger.error(f"Error generando SAS URL para '{blob_name}': {e}")
raise e
# Instancia global del servicio
azure_service = AzureBlobService()