add redis backend
This commit is contained in:
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