add redis backend
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
from typing import List
|
||||
from pydantic import validator
|
||||
|
||||
from pydantic import RedisDsn
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
@@ -8,20 +8,22 @@ class Settings(BaseSettings):
|
||||
"""
|
||||
Configuración básica de la aplicación
|
||||
"""
|
||||
|
||||
|
||||
# Configuración básica de la aplicación
|
||||
APP_NAME: str = "File Manager API"
|
||||
DEBUG: bool = False
|
||||
HOST: str = "0.0.0.0"
|
||||
PORT: int = 8000
|
||||
|
||||
|
||||
# Configuración de CORS para React frontend
|
||||
ALLOWED_ORIGINS: List[str] = [
|
||||
"http://localhost:3000", # React dev server
|
||||
"http://localhost:5173",
|
||||
"http://frontend:3000", # Docker container name
|
||||
"http://frontend:3000", # Docker container name
|
||||
]
|
||||
|
||||
|
||||
REDIS_OM_URL: RedisDsn
|
||||
|
||||
# Azure Blob Storage configuración
|
||||
AZURE_STORAGE_CONNECTION_STRING: str
|
||||
AZURE_STORAGE_ACCOUNT_NAME: str = ""
|
||||
@@ -52,66 +54,10 @@ class Settings(BaseSettings):
|
||||
# Schemas storage
|
||||
SCHEMAS_DIR: str = "./data/schemas"
|
||||
|
||||
@validator("AZURE_STORAGE_CONNECTION_STRING")
|
||||
def validate_azure_connection_string(cls, v):
|
||||
"""Validar que el connection string de Azure esté presente"""
|
||||
if not v:
|
||||
raise ValueError("AZURE_STORAGE_CONNECTION_STRING es requerido")
|
||||
return v
|
||||
|
||||
@validator("QDRANT_URL")
|
||||
def validate_qdrant_url(cls, v):
|
||||
"""Validar que la URL de Qdrant esté presente"""
|
||||
if not v:
|
||||
raise ValueError("QDRANT_URL es requerido")
|
||||
return v
|
||||
|
||||
@validator("QDRANT_API_KEY")
|
||||
def validate_qdrant_api_key(cls, v):
|
||||
"""Validar que la API key de Qdrant esté presente"""
|
||||
if not v:
|
||||
raise ValueError("QDRANT_API_KEY es requerido")
|
||||
return v
|
||||
|
||||
@validator("AZURE_OPENAI_ENDPOINT")
|
||||
def validate_azure_openai_endpoint(cls, v):
|
||||
"""Validar que el endpoint de Azure OpenAI esté presente"""
|
||||
if not v:
|
||||
raise ValueError("AZURE_OPENAI_ENDPOINT es requerido")
|
||||
return v
|
||||
|
||||
@validator("AZURE_OPENAI_API_KEY")
|
||||
def validate_azure_openai_api_key(cls, v):
|
||||
"""Validar que la API key de Azure OpenAI esté presente"""
|
||||
if not v:
|
||||
raise ValueError("AZURE_OPENAI_API_KEY es requerido")
|
||||
return v
|
||||
|
||||
@validator("GOOGLE_APPLICATION_CREDENTIALS")
|
||||
def validate_google_credentials(cls, v):
|
||||
"""Validar que el path de credenciales de Google esté presente"""
|
||||
if not v:
|
||||
raise ValueError("GOOGLE_APPLICATION_CREDENTIALS es requerido")
|
||||
return v
|
||||
|
||||
@validator("GOOGLE_CLOUD_PROJECT")
|
||||
def validate_google_project(cls, v):
|
||||
"""Validar que el proyecto de Google Cloud esté presente"""
|
||||
if not v:
|
||||
raise ValueError("GOOGLE_CLOUD_PROJECT es requerido")
|
||||
return v
|
||||
|
||||
@validator("LANDINGAI_API_KEY")
|
||||
def validate_landingai_api_key(cls, v):
|
||||
"""Validar que la API key de LandingAI esté presente"""
|
||||
if not v:
|
||||
raise ValueError("LANDINGAI_API_KEY es requerido")
|
||||
return v
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
# Instancia global de configuración
|
||||
settings = Settings()
|
||||
settings = Settings.model_validate({})
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
import uvicorn
|
||||
import logging
|
||||
|
||||
from .core.config import settings
|
||||
from .routers.chunking import router as chunking_router
|
||||
from .routers.chunking_landingai import router as chunking_landingai_router
|
||||
from .routers.dataroom import router as dataroom_router
|
||||
|
||||
# Import routers
|
||||
from .routers.files import router as files_router
|
||||
from .routers.vectors import router as vectors_router
|
||||
from .routers.chunking import router as chunking_router
|
||||
from .routers.schemas import router as schemas_router
|
||||
from .routers.chunking_landingai import router as chunking_landingai_router
|
||||
from .core.config import settings
|
||||
from .routers.vectors import router as vectors_router
|
||||
|
||||
# from routers.ai import router as ai_router # futuro con Azure OpenAI
|
||||
|
||||
# Import config
|
||||
@@ -18,18 +23,31 @@ from .core.config import settings
|
||||
|
||||
# Configurar logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
logger.info("Iniciando File Manager API...")
|
||||
logger.info(
|
||||
f"Conectando a Azure Storage Account: {settings.AZURE_STORAGE_ACCOUNT_NAME}"
|
||||
)
|
||||
logger.info(f"Conectando a Qdrant: {settings.QDRANT_URL}")
|
||||
|
||||
yield
|
||||
|
||||
logger.info("Cerrando File Manager API...")
|
||||
# Cleanup de recursos si es necesario
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="File Manager API",
|
||||
description=" DoRa",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
# Configurar CORS para React frontend
|
||||
@@ -41,6 +59,7 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# Middleware para logging de requests
|
||||
@app.middleware("http")
|
||||
async def log_requests(request, call_next):
|
||||
@@ -49,19 +68,17 @@ async def log_requests(request, call_next):
|
||||
logger.info(f"Response: {response.status_code}")
|
||||
return response
|
||||
|
||||
|
||||
# Manejador global de excepciones
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request, exc):
|
||||
logger.error(f"HTTP Exception: {exc.status_code} - {exc.detail}")
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
"error": True,
|
||||
"message": exc.detail,
|
||||
"status_code": exc.status_code
|
||||
}
|
||||
content={"error": True, "message": exc.detail, "status_code": exc.status_code},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def general_exception_handler(request, exc):
|
||||
logger.error(f"Unhandled Exception: {str(exc)}")
|
||||
@@ -70,10 +87,11 @@ async def general_exception_handler(request, exc):
|
||||
content={
|
||||
"error": True,
|
||||
"message": "Error interno del servidor",
|
||||
"status_code": 500
|
||||
}
|
||||
"status_code": 500,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Health check endpoint
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
@@ -81,9 +99,10 @@ async def health_check():
|
||||
return {
|
||||
"status": "healthy",
|
||||
"message": "File Manager API está funcionando correctamente",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.0",
|
||||
}
|
||||
|
||||
|
||||
# Root endpoint
|
||||
@app.get("/")
|
||||
async def root():
|
||||
@@ -92,27 +111,16 @@ async def root():
|
||||
"message": "File Manager API",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs",
|
||||
"health": "/health"
|
||||
"health": "/health",
|
||||
}
|
||||
|
||||
|
||||
# Incluir routers
|
||||
app.include_router(
|
||||
files_router,
|
||||
prefix="/api/v1/files",
|
||||
tags=["files"]
|
||||
)
|
||||
app.include_router(files_router, prefix="/api/v1/files", tags=["files"])
|
||||
|
||||
app.include_router(
|
||||
vectors_router,
|
||||
prefix="/api/v1",
|
||||
tags=["vectors"]
|
||||
)
|
||||
app.include_router(vectors_router, prefix="/api/v1", tags=["vectors"])
|
||||
|
||||
app.include_router(
|
||||
chunking_router,
|
||||
prefix="/api/v1",
|
||||
tags=["chunking"]
|
||||
)
|
||||
app.include_router(chunking_router, prefix="/api/v1", tags=["chunking"])
|
||||
|
||||
# Schemas router (nuevo)
|
||||
app.include_router(schemas_router)
|
||||
@@ -120,6 +128,8 @@ app.include_router(schemas_router)
|
||||
# Chunking LandingAI router (nuevo)
|
||||
app.include_router(chunking_landingai_router)
|
||||
|
||||
app.include_router(dataroom_router, prefix="/api/v1")
|
||||
|
||||
# Router para IA
|
||||
# app.include_router(
|
||||
# ai_router,
|
||||
@@ -127,21 +137,6 @@ app.include_router(chunking_landingai_router)
|
||||
# tags=["ai"]
|
||||
# )
|
||||
|
||||
# Evento de startup
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
logger.info("Iniciando File Manager API...")
|
||||
logger.info(f"Conectando a Azure Storage Account: {settings.AZURE_STORAGE_ACCOUNT_NAME}")
|
||||
logger.info(f"Conectando a Qdrant: {settings.QDRANT_URL}")
|
||||
# validaciones de conexión a Azure
|
||||
|
||||
|
||||
# Evento de shutdown
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
logger.info("Cerrando File Manager API...")
|
||||
# Cleanup de recursos si es necesario
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
@@ -149,5 +144,5 @@ if __name__ == "__main__":
|
||||
host=settings.HOST,
|
||||
port=settings.PORT,
|
||||
reload=settings.DEBUG,
|
||||
log_level="info"
|
||||
)
|
||||
log_level="info",
|
||||
)
|
||||
|
||||
10
backend/app/models/dataroom.py
Normal file
10
backend/app/models/dataroom.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from redis_om import HashModel, Migrator
|
||||
|
||||
|
||||
class DataRoom(HashModel):
|
||||
name: str
|
||||
collection: str
|
||||
storage: str
|
||||
|
||||
|
||||
Migrator().run()
|
||||
150
backend/app/routers/dataroom.py
Normal file
150
backend/app/routers/dataroom.py
Normal file
@@ -0,0 +1,150 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..models.dataroom import DataRoom
|
||||
from ..models.vector_models import CollectionCreateRequest
|
||||
from ..services.vector_service import vector_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DataroomCreate(BaseModel):
|
||||
name: str
|
||||
collection: str = ""
|
||||
storage: str = ""
|
||||
|
||||
|
||||
router = APIRouter(prefix="/dataroom", tags=["Dataroom"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_datarooms():
|
||||
"""
|
||||
Listar todos los temas disponibles
|
||||
"""
|
||||
try:
|
||||
# Get all DataRoom instances
|
||||
datarooms: list[DataRoom] = DataRoom.find().all()
|
||||
logger.info(f"Found {len(datarooms)} datarooms in Redis")
|
||||
|
||||
# Convert to list of dictionaries
|
||||
dataroom_list = [
|
||||
{"name": room.name, "collection": room.collection, "storage": room.storage}
|
||||
for room in datarooms
|
||||
]
|
||||
|
||||
logger.info(f"Returning dataroom list: {dataroom_list}")
|
||||
return {"datarooms": dataroom_list}
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing datarooms: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error listing datarooms: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_dataroom(dataroom: DataroomCreate):
|
||||
"""
|
||||
Crear un nuevo dataroom y su colección vectorial asociada
|
||||
"""
|
||||
try:
|
||||
# Create new DataRoom instance
|
||||
new_dataroom = DataRoom(
|
||||
name=dataroom.name, collection=dataroom.collection, storage=dataroom.storage
|
||||
)
|
||||
|
||||
# Save to Redis
|
||||
new_dataroom.save()
|
||||
|
||||
# Create the vector collection for this dataroom
|
||||
try:
|
||||
# First check if collection already exists
|
||||
collection_exists_response = await vector_service.check_collection_exists(
|
||||
dataroom.name
|
||||
)
|
||||
|
||||
if not collection_exists_response.exists:
|
||||
# Only create if it doesn't exist
|
||||
collection_request = CollectionCreateRequest(
|
||||
collection_name=dataroom.name,
|
||||
vector_size=3072, # Default vector size for embeddings
|
||||
distance="Cosine", # Default distance metric
|
||||
)
|
||||
await vector_service.create_collection(collection_request)
|
||||
logger.info(f"Collection '{dataroom.name}' created successfully")
|
||||
else:
|
||||
logger.info(
|
||||
f"Collection '{dataroom.name}' already exists, skipping creation"
|
||||
)
|
||||
except Exception as e:
|
||||
# Log the error but don't fail the dataroom creation
|
||||
logger.warning(
|
||||
f"Could not create collection for dataroom '{dataroom.name}': {e}"
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Dataroom created successfully",
|
||||
"dataroom": {
|
||||
"name": new_dataroom.name,
|
||||
"collection": new_dataroom.collection,
|
||||
"storage": new_dataroom.storage,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error creating dataroom: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{dataroom_name}")
|
||||
async def delete_dataroom(dataroom_name: str):
|
||||
"""
|
||||
Eliminar un dataroom y su colección vectorial asociada
|
||||
"""
|
||||
try:
|
||||
# First check if dataroom exists
|
||||
existing_datarooms = DataRoom.find().all()
|
||||
dataroom_exists = any(room.name == dataroom_name for room in existing_datarooms)
|
||||
|
||||
if not dataroom_exists:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Dataroom '{dataroom_name}' not found"
|
||||
)
|
||||
|
||||
# Delete the vector collection first
|
||||
try:
|
||||
collection_exists = await vector_service.check_collection_exists(
|
||||
dataroom_name
|
||||
)
|
||||
if collection_exists.exists:
|
||||
await vector_service.delete_collection(dataroom_name)
|
||||
logger.info(
|
||||
f"Collection '{dataroom_name}' deleted from vector database"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Could not delete collection '{dataroom_name}' from vector database: {e}"
|
||||
)
|
||||
# Continue with dataroom deletion even if collection deletion fails
|
||||
|
||||
# Delete the dataroom from Redis
|
||||
for room in existing_datarooms:
|
||||
if room.name == dataroom_name:
|
||||
# Delete using the primary key
|
||||
DataRoom.delete(room.pk)
|
||||
logger.info(f"Dataroom '{dataroom_name}' deleted from Redis")
|
||||
break
|
||||
|
||||
return {
|
||||
"message": "Dataroom deleted successfully",
|
||||
"dataroom_name": dataroom_name,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting dataroom '{dataroom_name}': {e}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error deleting dataroom: {str(e)}"
|
||||
)
|
||||
@@ -1,18 +1,28 @@
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException, Query, Form
|
||||
from fastapi.responses import StreamingResponse, Response
|
||||
from typing import Optional, List
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import zipfile
|
||||
import io
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, File, Form, HTTPException, Query, UploadFile
|
||||
from fastapi.responses import Response, StreamingResponse
|
||||
|
||||
from ..models.dataroom import DataRoom
|
||||
from ..models.file_models import (
|
||||
FileUploadRequest, FileUploadResponse, FileInfo, FileListResponse,
|
||||
FileDeleteResponse, FileBatchDeleteRequest,
|
||||
FileConflictResponse, FileBatchDeleteResponse,
|
||||
FileBatchDownloadRequest, TemasListResponse,
|
||||
FileUploadCheckRequest, FileUploadConfirmRequest, ErrorResponse
|
||||
ErrorResponse,
|
||||
FileBatchDeleteRequest,
|
||||
FileBatchDeleteResponse,
|
||||
FileBatchDownloadRequest,
|
||||
FileConflictResponse,
|
||||
FileDeleteResponse,
|
||||
FileInfo,
|
||||
FileListResponse,
|
||||
FileUploadCheckRequest,
|
||||
FileUploadConfirmRequest,
|
||||
FileUploadRequest,
|
||||
FileUploadResponse,
|
||||
TemasListResponse,
|
||||
)
|
||||
from ..services.azure_service import azure_service
|
||||
from ..services.file_service import file_service
|
||||
@@ -31,27 +41,27 @@ async def check_file_before_upload(request: FileUploadCheckRequest):
|
||||
is_valid, error_msg = file_service.validate_filename(request.filename)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
||||
|
||||
# Validar extensión
|
||||
is_valid, error_msg = file_service.validate_file_extension(request.filename)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
||||
|
||||
# Limpiar tema
|
||||
clean_tema = file_service.clean_tema_name(request.tema or "")
|
||||
|
||||
|
||||
# Verificar si existe conflicto
|
||||
has_conflict, suggested_name = await file_service.handle_file_conflict(
|
||||
request.filename, clean_tema
|
||||
)
|
||||
|
||||
|
||||
if has_conflict:
|
||||
return FileConflictResponse(
|
||||
conflict=True,
|
||||
message=f"El archivo '{request.filename}' ya existe en el tema '{clean_tema or 'general'}'",
|
||||
existing_file=request.filename,
|
||||
suggested_name=suggested_name,
|
||||
tema=clean_tema
|
||||
tema=clean_tema,
|
||||
)
|
||||
else:
|
||||
# No hay conflicto, se puede subir directamente
|
||||
@@ -60,14 +70,16 @@ async def check_file_before_upload(request: FileUploadCheckRequest):
|
||||
message="Archivo disponible para subir",
|
||||
existing_file=request.filename,
|
||||
suggested_name=request.filename,
|
||||
tema=clean_tema
|
||||
tema=clean_tema,
|
||||
)
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error verificando archivo '{request.filename}': {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/upload/confirm", response_model=FileUploadResponse)
|
||||
@@ -75,7 +87,7 @@ async def upload_file_with_confirmation(
|
||||
file: UploadFile = File(...),
|
||||
action: str = Form(...),
|
||||
tema: Optional[str] = Form(None),
|
||||
new_filename: Optional[str] = Form(None)
|
||||
new_filename: Optional[str] = Form(None),
|
||||
):
|
||||
"""
|
||||
Subir archivo con confirmación de acción para conflictos
|
||||
@@ -84,61 +96,54 @@ async def upload_file_with_confirmation(
|
||||
# Validar archivo
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="Nombre de archivo requerido")
|
||||
|
||||
|
||||
# Crear request de confirmación para validaciones
|
||||
confirm_request = FileUploadConfirmRequest(
|
||||
filename=file.filename,
|
||||
tema=tema,
|
||||
action=action,
|
||||
new_filename=new_filename
|
||||
filename=file.filename, tema=tema, action=action, new_filename=new_filename
|
||||
)
|
||||
|
||||
|
||||
# Si la acción es cancelar, no hacer nada
|
||||
if confirm_request.action == "cancel":
|
||||
return FileUploadResponse(
|
||||
success=False,
|
||||
message="Subida cancelada por el usuario",
|
||||
file=None
|
||||
success=False, message="Subida cancelada por el usuario", file=None
|
||||
)
|
||||
|
||||
|
||||
# Determinar el nombre final del archivo
|
||||
final_filename = file.filename
|
||||
if confirm_request.action == "rename" and confirm_request.new_filename:
|
||||
final_filename = confirm_request.new_filename
|
||||
|
||||
|
||||
# Validar extensión del archivo final
|
||||
is_valid, error_msg = file_service.validate_file_extension(final_filename)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
||||
|
||||
# Leer contenido del archivo
|
||||
file_content = await file.read()
|
||||
|
||||
|
||||
# Validar tamaño del archivo
|
||||
is_valid, error_msg = file_service.validate_file_size(len(file_content))
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
||||
|
||||
# Limpiar tema
|
||||
clean_tema = file_service.clean_tema_name(confirm_request.tema or "")
|
||||
|
||||
|
||||
# Si es sobrescribir, verificar que el archivo original exista
|
||||
if confirm_request.action == "overwrite":
|
||||
exists = await file_service.check_file_exists(file.filename, clean_tema)
|
||||
if not exists:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Archivo '{file.filename}' no existe para sobrescribir"
|
||||
status_code=404,
|
||||
detail=f"Archivo '{file.filename}' no existe para sobrescribir",
|
||||
)
|
||||
|
||||
|
||||
# Subir archivo a Azure
|
||||
file_stream = io.BytesIO(file_content)
|
||||
uploaded_file_info = await azure_service.upload_file(
|
||||
file_data=file_stream,
|
||||
blob_name=final_filename,
|
||||
tema=clean_tema
|
||||
file_data=file_stream, blob_name=final_filename, tema=clean_tema
|
||||
)
|
||||
|
||||
|
||||
# Crear objeto FileInfo
|
||||
file_info = FileInfo(
|
||||
name=uploaded_file_info["name"],
|
||||
@@ -146,75 +151,95 @@ async def upload_file_with_confirmation(
|
||||
tema=uploaded_file_info["tema"],
|
||||
size=uploaded_file_info["size"],
|
||||
last_modified=uploaded_file_info["last_modified"],
|
||||
url=uploaded_file_info["url"]
|
||||
url=uploaded_file_info["url"],
|
||||
)
|
||||
|
||||
|
||||
action_msg = {
|
||||
"overwrite": "sobrescrito",
|
||||
"rename": f"renombrado a '{final_filename}'"
|
||||
"rename": f"renombrado a '{final_filename}'",
|
||||
}
|
||||
|
||||
logger.info(f"Archivo '{file.filename}' {action_msg.get(confirm_request.action, 'subido')} exitosamente")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Archivo '{file.filename}' {action_msg.get(confirm_request.action, 'subido')} exitosamente"
|
||||
)
|
||||
|
||||
return FileUploadResponse(
|
||||
success=True,
|
||||
message=f"Archivo {action_msg.get(confirm_request.action, 'subido')} exitosamente",
|
||||
file=file_info
|
||||
file=file_info,
|
||||
)
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error en subida confirmada: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/upload", response_model=FileUploadResponse)
|
||||
async def upload_file(
|
||||
file: UploadFile = File(...),
|
||||
tema: Optional[str] = Form(None)
|
||||
):
|
||||
async def upload_file(file: UploadFile = File(...), tema: Optional[str] = Form(None)):
|
||||
"""
|
||||
Subir un archivo al almacenamiento
|
||||
"""
|
||||
try:
|
||||
# Validar que el dataroom existe si se proporciona un tema
|
||||
if tema:
|
||||
existing_datarooms = DataRoom.find().all()
|
||||
dataroom_exists = any(room.name == tema for room in existing_datarooms)
|
||||
|
||||
if not dataroom_exists:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"El dataroom '{tema}' no existe. Créalo primero antes de subir archivos.",
|
||||
)
|
||||
|
||||
# Validar archivo
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="Nombre de archivo requerido")
|
||||
|
||||
|
||||
# Validar extensión del archivo
|
||||
file_extension = os.path.splitext(file.filename)[1].lower()
|
||||
allowed_extensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.csv']
|
||||
|
||||
allowed_extensions = [
|
||||
".pdf",
|
||||
".doc",
|
||||
".docx",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".txt",
|
||||
".csv",
|
||||
]
|
||||
|
||||
if file_extension not in allowed_extensions:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Tipo de archivo no permitido. Extensiones permitidas: {', '.join(allowed_extensions)}"
|
||||
status_code=400,
|
||||
detail=f"Tipo de archivo no permitido. Extensiones permitidas: {', '.join(allowed_extensions)}",
|
||||
)
|
||||
|
||||
|
||||
# Leer contenido del archivo
|
||||
file_content = await file.read()
|
||||
|
||||
|
||||
# Validar tamaño del archivo (100MB máximo)
|
||||
max_size = 100 * 1024 * 1024 # 100MB
|
||||
if len(file_content) > max_size:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Archivo demasiado grande. Tamaño máximo permitido: 100MB"
|
||||
detail=f"Archivo demasiado grande. Tamaño máximo permitido: 100MB",
|
||||
)
|
||||
|
||||
|
||||
# Procesar tema
|
||||
upload_request = FileUploadRequest(tema=tema)
|
||||
processed_tema = upload_request.tema or ""
|
||||
|
||||
|
||||
# Subir archivo a Azure
|
||||
file_stream = io.BytesIO(file_content)
|
||||
uploaded_file_info = await azure_service.upload_file(
|
||||
file_data=file_stream,
|
||||
blob_name=file.filename,
|
||||
tema=processed_tema
|
||||
file_data=file_stream, blob_name=file.filename, tema=processed_tema
|
||||
)
|
||||
|
||||
|
||||
# Crear objeto FileInfo
|
||||
file_info = FileInfo(
|
||||
name=uploaded_file_info["name"],
|
||||
@@ -222,22 +247,24 @@ async def upload_file(
|
||||
tema=uploaded_file_info["tema"],
|
||||
size=uploaded_file_info["size"],
|
||||
last_modified=uploaded_file_info["last_modified"],
|
||||
url=uploaded_file_info["url"]
|
||||
url=uploaded_file_info["url"],
|
||||
)
|
||||
|
||||
logger.info(f"Archivo '{file.filename}' subido exitosamente al tema '{processed_tema}'")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Archivo '{file.filename}' subido exitosamente al tema '{processed_tema}'"
|
||||
)
|
||||
|
||||
return FileUploadResponse(
|
||||
success=True,
|
||||
message="Archivo subido exitosamente",
|
||||
file=file_info
|
||||
success=True, message="Archivo subido exitosamente", file=file_info
|
||||
)
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error subiendo archivo: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=FileListResponse)
|
||||
@@ -248,7 +275,7 @@ async def list_files(tema: Optional[str] = Query(None, description="Filtrar por
|
||||
try:
|
||||
# Obtener archivos de Azure
|
||||
files_data = await azure_service.list_files(tema=tema or "")
|
||||
|
||||
|
||||
# Convertir a objetos FileInfo
|
||||
files_info = []
|
||||
for file_data in files_data:
|
||||
@@ -258,21 +285,22 @@ async def list_files(tema: Optional[str] = Query(None, description="Filtrar por
|
||||
tema=file_data["tema"],
|
||||
size=file_data["size"],
|
||||
last_modified=file_data["last_modified"],
|
||||
content_type=file_data.get("content_type")
|
||||
content_type=file_data.get("content_type"),
|
||||
)
|
||||
files_info.append(file_info)
|
||||
|
||||
logger.info(f"Listados {len(files_info)} archivos" + (f" del tema '{tema}'" if tema else ""))
|
||||
|
||||
return FileListResponse(
|
||||
files=files_info,
|
||||
total=len(files_info),
|
||||
tema=tema
|
||||
|
||||
logger.info(
|
||||
f"Listados {len(files_info)} archivos"
|
||||
+ (f" del tema '{tema}'" if tema else "")
|
||||
)
|
||||
|
||||
|
||||
return FileListResponse(files=files_info, total=len(files_info), tema=tema)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listando archivos: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/temas", response_model=TemasListResponse)
|
||||
@@ -283,31 +311,30 @@ async def list_temas():
|
||||
try:
|
||||
# Obtener todos los archivos
|
||||
files_data = await azure_service.list_files()
|
||||
|
||||
|
||||
# Extraer temas únicos
|
||||
temas = set()
|
||||
for file_data in files_data:
|
||||
if file_data["tema"]:
|
||||
temas.add(file_data["tema"])
|
||||
|
||||
|
||||
temas_list = sorted(list(temas))
|
||||
|
||||
|
||||
logger.info(f"Encontrados {len(temas_list)} temas")
|
||||
|
||||
return TemasListResponse(
|
||||
temas=temas_list,
|
||||
total=len(temas_list)
|
||||
)
|
||||
|
||||
|
||||
return TemasListResponse(temas=temas_list, total=len(temas_list))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listando temas: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{filename}/download")
|
||||
async def download_file(
|
||||
filename: str,
|
||||
tema: Optional[str] = Query(None, description="Tema donde está el archivo")
|
||||
tema: Optional[str] = Query(None, description="Tema donde está el archivo"),
|
||||
):
|
||||
"""
|
||||
Descargar un archivo individual
|
||||
@@ -315,64 +342,71 @@ async def download_file(
|
||||
try:
|
||||
# Descargar archivo de Azure
|
||||
file_content = await azure_service.download_file(
|
||||
blob_name=filename,
|
||||
tema=tema or ""
|
||||
blob_name=filename, tema=tema or ""
|
||||
)
|
||||
|
||||
|
||||
# Obtener información del archivo para content-type
|
||||
file_info = await azure_service.get_file_info(
|
||||
blob_name=filename,
|
||||
tema=tema or ""
|
||||
blob_name=filename, tema=tema or ""
|
||||
)
|
||||
|
||||
|
||||
# Determinar content-type
|
||||
content_type = file_info.get("content_type", "application/octet-stream")
|
||||
|
||||
logger.info(f"Descargando archivo '{filename}'" + (f" del tema '{tema}'" if tema else ""))
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Descargando archivo '{filename}'"
|
||||
+ (f" del tema '{tema}'" if tema else "")
|
||||
)
|
||||
|
||||
return Response(
|
||||
content=file_content,
|
||||
media_type=content_type,
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}"
|
||||
}
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||||
)
|
||||
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Archivo '{filename}' no encontrado")
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Archivo '{filename}' no encontrado"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error descargando archivo '{filename}': {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{filename}", response_model=FileDeleteResponse)
|
||||
async def delete_file(
|
||||
filename: str,
|
||||
tema: Optional[str] = Query(None, description="Tema donde está el archivo")
|
||||
tema: Optional[str] = Query(None, description="Tema donde está el archivo"),
|
||||
):
|
||||
"""
|
||||
Eliminar un archivo
|
||||
"""
|
||||
try:
|
||||
# Eliminar archivo de Azure
|
||||
await azure_service.delete_file(
|
||||
blob_name=filename,
|
||||
tema=tema or ""
|
||||
await azure_service.delete_file(blob_name=filename, tema=tema or "")
|
||||
|
||||
logger.info(
|
||||
f"Archivo '{filename}' eliminado exitosamente"
|
||||
+ (f" del tema '{tema}'" if tema else "")
|
||||
)
|
||||
|
||||
logger.info(f"Archivo '{filename}' eliminado exitosamente" + (f" del tema '{tema}'" if tema else ""))
|
||||
|
||||
|
||||
return FileDeleteResponse(
|
||||
success=True,
|
||||
message="Archivo eliminado exitosamente",
|
||||
deleted_file=filename
|
||||
deleted_file=filename,
|
||||
)
|
||||
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Archivo '{filename}' no encontrado")
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Archivo '{filename}' no encontrado"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error eliminando archivo '{filename}': {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/delete-batch", response_model=FileBatchDeleteResponse)
|
||||
@@ -383,34 +417,35 @@ async def delete_batch_files(request: FileBatchDeleteRequest):
|
||||
try:
|
||||
deleted_files = []
|
||||
failed_files = []
|
||||
|
||||
|
||||
for filename in request.files:
|
||||
try:
|
||||
await azure_service.delete_file(
|
||||
blob_name=filename,
|
||||
tema=request.tema or ""
|
||||
blob_name=filename, tema=request.tema or ""
|
||||
)
|
||||
deleted_files.append(filename)
|
||||
logger.info(f"Archivo '{filename}' eliminado exitosamente")
|
||||
except Exception as e:
|
||||
failed_files.append(filename)
|
||||
logger.error(f"Error eliminando archivo '{filename}': {e}")
|
||||
|
||||
|
||||
success = len(failed_files) == 0
|
||||
message = f"Eliminados {len(deleted_files)} archivos exitosamente"
|
||||
if failed_files:
|
||||
message += f", {len(failed_files)} archivos fallaron"
|
||||
|
||||
|
||||
return FileBatchDeleteResponse(
|
||||
success=success,
|
||||
message=message,
|
||||
deleted_files=deleted_files,
|
||||
failed_files=failed_files
|
||||
failed_files=failed_files,
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error en eliminación batch: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/download-batch")
|
||||
@@ -421,44 +456,43 @@ async def download_batch_files(request: FileBatchDownloadRequest):
|
||||
try:
|
||||
# Crear ZIP en memoria
|
||||
zip_buffer = io.BytesIO()
|
||||
|
||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
|
||||
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for filename in request.files:
|
||||
try:
|
||||
# Descargar archivo de Azure
|
||||
file_content = await azure_service.download_file(
|
||||
blob_name=filename,
|
||||
tema=request.tema or ""
|
||||
blob_name=filename, tema=request.tema or ""
|
||||
)
|
||||
|
||||
|
||||
# Agregar al ZIP
|
||||
zip_file.writestr(filename, file_content)
|
||||
logger.info(f"Archivo '{filename}' agregado al ZIP")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error agregando '{filename}' al ZIP: {e}")
|
||||
# Continuar con otros archivos
|
||||
continue
|
||||
|
||||
|
||||
zip_buffer.seek(0)
|
||||
|
||||
|
||||
# Generar nombre del ZIP
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
zip_filename = f"{request.zip_name}_{timestamp}.zip"
|
||||
|
||||
|
||||
logger.info(f"ZIP creado exitosamente: {zip_filename}")
|
||||
|
||||
|
||||
return StreamingResponse(
|
||||
io.BytesIO(zip_buffer.read()),
|
||||
media_type="application/zip",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={zip_filename}"
|
||||
}
|
||||
headers={"Content-Disposition": f"attachment; filename={zip_filename}"},
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creando ZIP: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tema/{tema}/download-all")
|
||||
@@ -469,54 +503,58 @@ async def download_tema_completo(tema: str):
|
||||
try:
|
||||
# Obtener todos los archivos del tema
|
||||
files_data = await azure_service.list_files(tema=tema)
|
||||
|
||||
|
||||
if not files_data:
|
||||
raise HTTPException(status_code=404, detail=f"No se encontraron archivos en el tema '{tema}'")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No se encontraron archivos en el tema '{tema}'",
|
||||
)
|
||||
|
||||
# Crear ZIP en memoria
|
||||
zip_buffer = io.BytesIO()
|
||||
|
||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
|
||||
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for file_data in files_data:
|
||||
try:
|
||||
filename = file_data["name"]
|
||||
|
||||
|
||||
# Descargar archivo de Azure
|
||||
file_content = await azure_service.download_file(
|
||||
blob_name=filename,
|
||||
tema=tema
|
||||
blob_name=filename, tema=tema
|
||||
)
|
||||
|
||||
|
||||
# Agregar al ZIP
|
||||
zip_file.writestr(filename, file_content)
|
||||
logger.info(f"Archivo '{filename}' agregado al ZIP del tema '{tema}'")
|
||||
|
||||
logger.info(
|
||||
f"Archivo '{filename}' agregado al ZIP del tema '{tema}'"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error agregando '{filename}' al ZIP: {e}")
|
||||
# Continuar con otros archivos
|
||||
continue
|
||||
|
||||
|
||||
zip_buffer.seek(0)
|
||||
|
||||
|
||||
# Generar nombre del ZIP
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
zip_filename = f"{tema}_{timestamp}.zip"
|
||||
|
||||
|
||||
logger.info(f"ZIP del tema '{tema}' creado exitosamente: {zip_filename}")
|
||||
|
||||
|
||||
return StreamingResponse(
|
||||
io.BytesIO(zip_buffer.read()),
|
||||
media_type="application/zip",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={zip_filename}"
|
||||
}
|
||||
headers={"Content-Disposition": f"attachment; filename={zip_filename}"},
|
||||
)
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creando ZIP del tema '{tema}': {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/tema/{tema}/delete-all", response_model=FileBatchDeleteResponse)
|
||||
@@ -527,51 +565,59 @@ async def delete_tema_completo(tema: str):
|
||||
try:
|
||||
# Obtener todos los archivos del tema
|
||||
files_data = await azure_service.list_files(tema=tema)
|
||||
|
||||
|
||||
if not files_data:
|
||||
raise HTTPException(status_code=404, detail=f"No se encontraron archivos en el tema '{tema}'")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No se encontraron archivos en el tema '{tema}'",
|
||||
)
|
||||
|
||||
deleted_files = []
|
||||
failed_files = []
|
||||
|
||||
|
||||
for file_data in files_data:
|
||||
filename = file_data["name"]
|
||||
try:
|
||||
await azure_service.delete_file(
|
||||
blob_name=filename,
|
||||
tema=tema
|
||||
)
|
||||
await azure_service.delete_file(blob_name=filename, tema=tema)
|
||||
deleted_files.append(filename)
|
||||
logger.info(f"Archivo '{filename}' eliminado del tema '{tema}'")
|
||||
except Exception as e:
|
||||
failed_files.append(filename)
|
||||
logger.error(f"Error eliminando archivo '{filename}' del tema '{tema}': {e}")
|
||||
|
||||
logger.error(
|
||||
f"Error eliminando archivo '{filename}' del tema '{tema}': {e}"
|
||||
)
|
||||
|
||||
success = len(failed_files) == 0
|
||||
message = f"Tema '{tema}': eliminados {len(deleted_files)} archivos exitosamente"
|
||||
message = (
|
||||
f"Tema '{tema}': eliminados {len(deleted_files)} archivos exitosamente"
|
||||
)
|
||||
if failed_files:
|
||||
message += f", {len(failed_files)} archivos fallaron"
|
||||
|
||||
logger.info(f"Eliminación completa del tema '{tema}': {len(deleted_files)} exitosos, {len(failed_files)} fallidos")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Eliminación completa del tema '{tema}': {len(deleted_files)} exitosos, {len(failed_files)} fallidos"
|
||||
)
|
||||
|
||||
return FileBatchDeleteResponse(
|
||||
success=success,
|
||||
message=message,
|
||||
deleted_files=deleted_files,
|
||||
failed_files=failed_files
|
||||
failed_files=failed_files,
|
||||
)
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error eliminando tema '{tema}': {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{filename}/info", response_model=FileInfo)
|
||||
async def get_file_info(
|
||||
filename: str,
|
||||
tema: Optional[str] = Query(None, description="Tema donde está el archivo")
|
||||
tema: Optional[str] = Query(None, description="Tema donde está el archivo"),
|
||||
):
|
||||
"""
|
||||
Obtener información detallada de un archivo
|
||||
@@ -579,8 +625,7 @@ async def get_file_info(
|
||||
try:
|
||||
# Obtener información de Azure
|
||||
file_data = await azure_service.get_file_info(
|
||||
blob_name=filename,
|
||||
tema=tema or ""
|
||||
blob_name=filename, tema=tema or ""
|
||||
)
|
||||
|
||||
# Convertir a objeto FileInfo
|
||||
@@ -591,24 +636,30 @@ async def get_file_info(
|
||||
size=file_data["size"],
|
||||
last_modified=file_data["last_modified"],
|
||||
content_type=file_data.get("content_type"),
|
||||
url=file_data.get("url")
|
||||
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")
|
||||
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)}")
|
||||
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)
|
||||
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
|
||||
@@ -633,23 +684,28 @@ async def get_file_preview_url(
|
||||
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
|
||||
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 ""))
|
||||
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 ''})"
|
||||
"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")
|
||||
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)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error interno del servidor: {str(e)}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user