124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
import logging
|
|
|
|
from fastapi import HTTPException, UploadFile
|
|
|
|
from common.db import execute, fetchall, fetchone, insert_file_row
|
|
from integrations.azure_blob import AzureBlobUploader
|
|
from models.file import FileListItem, FileType, FileUploadResponse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_MAX_UPLOAD_BYTES = 50 * 1024 * 1024 # 50MB
|
|
|
|
|
|
async def upload_file_to_blob(
|
|
content: bytes,
|
|
file_name: str,
|
|
group: str,
|
|
category: str = "file",
|
|
content_type: str | None = None,
|
|
) -> str:
|
|
"""Azure Blob에 파일을 업로드하고 public URL을 반환. DB는 건드리지 않음."""
|
|
uploader = AzureBlobUploader(group=group, category=category)
|
|
return await uploader.upload_bytes(content=content, file_name=file_name, content_type=content_type)
|
|
|
|
|
|
async def upload_analysis_file(
|
|
analysis_run_id: str,
|
|
content: bytes,
|
|
file_name: str,
|
|
file_type: str = "file",
|
|
content_type: str | None = None,
|
|
) -> tuple[int, str]:
|
|
"""analysis_run에 딸린 파일 업로드. Blob 업로드 + file_data row 생성. (file_id, url) 반환."""
|
|
run = await fetchone(
|
|
"SELECT hospital_id FROM analysis_runs WHERE analysis_run_id = %s",
|
|
(analysis_run_id,),
|
|
)
|
|
if not run:
|
|
raise HTTPException(status_code=404, detail="analysis_run not found")
|
|
hospital_id = run["hospital_id"]
|
|
|
|
public_url = await upload_file_to_blob(
|
|
content=content,
|
|
file_name=file_name,
|
|
group=analysis_run_id,
|
|
category=file_type,
|
|
content_type=content_type,
|
|
)
|
|
|
|
file_id = await insert_file_row(
|
|
analysis_run_id=analysis_run_id,
|
|
hospital_id=hospital_id,
|
|
file_type=file_type,
|
|
file_name=file_name,
|
|
file_url=public_url,
|
|
size_bytes=len(content),
|
|
)
|
|
logger.info("uploaded analysis file run=%s file_id=%s url=%s", analysis_run_id, file_id, public_url)
|
|
return file_id, public_url
|
|
|
|
|
|
async def list_analysis_files(analysis_run_id: str) -> list[dict]:
|
|
"""analysis_run에 딸린 (삭제 안 된) 파일 목록."""
|
|
return await fetchall(
|
|
"SELECT id, file_type, file_name, file_url, size_bytes, created_at FROM file_data"
|
|
" WHERE analysis_run_id = %s AND is_deleted = FALSE"
|
|
" ORDER BY created_at DESC",
|
|
(analysis_run_id,),
|
|
)
|
|
|
|
|
|
async def handle_analysis_file_upload(
|
|
analysis_run_id: str,
|
|
upload: UploadFile,
|
|
file_type: FileType,
|
|
) -> FileUploadResponse:
|
|
"""multipart UploadFile 검증 + 업로드 + 응답 모델 생성."""
|
|
if not upload.filename:
|
|
raise HTTPException(status_code=400, detail="filename is required")
|
|
content = await upload.read()
|
|
if not content:
|
|
raise HTTPException(status_code=400, detail="empty file")
|
|
if len(content) > _MAX_UPLOAD_BYTES:
|
|
raise HTTPException(status_code=413, detail=f"file too large (max {_MAX_UPLOAD_BYTES} bytes)")
|
|
|
|
file_id, public_url = await upload_analysis_file(
|
|
analysis_run_id=analysis_run_id,
|
|
content=content,
|
|
file_name=upload.filename,
|
|
file_type=file_type.value,
|
|
content_type=upload.content_type,
|
|
)
|
|
return FileUploadResponse(
|
|
id=file_id,
|
|
analysis_run_id=analysis_run_id,
|
|
file_type=file_type,
|
|
file_name=upload.filename,
|
|
file_url=public_url,
|
|
size_bytes=len(content),
|
|
)
|
|
|
|
|
|
async def get_analysis_files_response(analysis_run_id: str) -> list[FileListItem]:
|
|
"""run 존재 확인 + 응답 모델 생성."""
|
|
if not await fetchone("SELECT 1 FROM analysis_runs WHERE analysis_run_id = %s", (analysis_run_id,)):
|
|
raise HTTPException(status_code=404, detail="analysis_run not found")
|
|
rows = await list_analysis_files(analysis_run_id)
|
|
return [FileListItem(**{**r, "created_at": str(r["created_at"])}) for r in rows]
|
|
|
|
|
|
async def soft_delete_analysis_file(analysis_run_id: str, file_id: int) -> None:
|
|
"""analysis_run에 딸린 파일을 소프트 삭제. 멱등성 보장."""
|
|
row = await fetchone(
|
|
"SELECT id FROM file_data WHERE id = %s AND analysis_run_id = %s",
|
|
(file_id, analysis_run_id),
|
|
)
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="file not found")
|
|
await execute(
|
|
"UPDATE file_data SET is_deleted = TRUE WHERE id = %s AND is_deleted = FALSE",
|
|
(file_id,),
|
|
)
|
|
logger.info("soft-deleted analysis file run=%s file_id=%s", analysis_run_id, file_id)
|