"""B03 input_files 테이블의 aiomysql Raw SQL 접근.""" import json from pathlib import PurePosixPath from typing import Any from uuid import UUID import aiomysql async def create_input_file( connection: aiomysql.Connection, *, project_id: UUID, file_type: str, original_filename: str, relative_path: str, file_size_bytes: int, upload_by: int | None, crs_epsg: int | None, metadata: dict[str, Any], ) -> int: """업로드 원본 파일 메타데이터를 저장하고 생성된 ID를 반환한다.""" normalized_path = PurePosixPath(relative_path) if normalized_path.is_absolute() or ".." in normalized_path.parts: raise ValueError("DB에는 프로젝트 루트 기준 상대 경로만 저장할 수 있습니다.") if normalized_path.parts[:2] != ("B03_FileInput", "input"): raise ValueError("입력 파일 경로는 B03_FileInput/input 아래여야 합니다.") async with connection.cursor() as cursor: await cursor.execute( """ INSERT INTO input_files ( project_id, file_type, original_filename, raw_file_path, file_size_mb, upload_by, crs_epsg, metadata, status ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'UPLOADED') """, ( str(project_id), file_type, original_filename, normalized_path.as_posix(), file_size_bytes / (1024 * 1024), upload_by, crs_epsg, json.dumps(metadata, ensure_ascii=False), ), ) input_file_id = cursor.lastrowid if not input_file_id: raise RuntimeError("input_files 레코드 생성 결과에 ID가 없습니다.") return int(input_file_id) async def get_project_storage_relative_path( connection: aiomysql.Connection, project_id: UUID, ) -> str: """프로젝트의 검증된 저장소 상대 경로를 조회한다.""" async with connection.cursor() as cursor: await cursor.execute( """ SELECT storage_path FROM projects WHERE id = %s AND deleted_at IS NULL """, (str(project_id),), ) row = await cursor.fetchone() if not row or not row[0]: raise LookupError("프로젝트 또는 프로젝트 저장 경로를 찾을 수 없습니다.") normalized_path = PurePosixPath(str(row[0]).replace("\\", "/")) if normalized_path.is_absolute() or ".." in normalized_path.parts: raise ValueError("프로젝트 저장 경로는 안전한 상대 경로여야 합니다.") return normalized_path.as_posix()