"""B03 파일 입력 저장 엔진.""" import os import tempfile from pathlib import Path from fastapi import UploadFile from B03_FileInput.B03_FileInput_Schema import FileUploadDescriptor from common_util.common_util_storage import get_project_stage_path from config.config_system import UPLOAD_CHUNK_SIZE_BYTES, UPLOAD_MAX_MB def resolve_upload_destination( project_root: str | Path, descriptor: FileUploadDescriptor, ) -> Path: """검증된 파일의 B03 입력 저장 경로를 생성해 반환한다.""" stage_root = Path(get_project_stage_path(str(project_root), "B03_FileInput")).resolve() file_type = Path(descriptor.original_filename).suffix.lower().lstrip(".") destination = (stage_root / "input" / file_type / descriptor.original_filename).resolve() if os.path.commonpath((stage_root, destination)) != str(stage_root): raise ValueError("업로드 저장 경로가 B03 단계 폴더를 벗어났습니다.") destination.parent.mkdir(parents=True, exist_ok=True) return destination async def save_upload_stream(upload: UploadFile, destination: Path) -> int: """업로드 스트림을 크기 제한 내에서 임시 파일에 기록한 뒤 교체한다.""" maximum_bytes = UPLOAD_MAX_MB * 1024 * 1024 written_bytes = 0 temporary_path: Path | None = None try: with tempfile.NamedTemporaryFile( mode="wb", dir=destination.parent, prefix=f".{destination.name}.", suffix=".upload", delete=False, ) as temporary: temporary_path = Path(temporary.name) while chunk := await upload.read(UPLOAD_CHUNK_SIZE_BYTES): written_bytes += len(chunk) if written_bytes > maximum_bytes: raise ValueError(f"파일 크기는 {UPLOAD_MAX_MB}MB를 초과할 수 없습니다.") temporary.write(chunk) temporary.flush() os.fsync(temporary.fileno()) if written_bytes == 0: raise ValueError("빈 파일은 업로드할 수 없습니다.") os.replace(temporary_path, destination) temporary_path = None return written_bytes finally: if temporary_path is not None: temporary_path.unlink(missing_ok=True)