diff --git a/B03_FileInput/B03_FileInput_Router_Temp.py b/B03_FileInput/B03_FileInput_Router_Temp.py index a190a34b..3304a0fe 100644 --- a/B03_FileInput/B03_FileInput_Router_Temp.py +++ b/B03_FileInput/B03_FileInput_Router_Temp.py @@ -13,53 +13,42 @@ from typing import Any from uuid import UUID, uuid4 import aiomysql -from fastapi import APIRouter, Depends, File, Form, UploadFile +from fastapi import APIRouter, Depends, File, UploadFile from fastapi.responses import JSONResponse from B03_FileInput.B03_FileInput_Engine import ( - merge_upload_chunks, - remove_chunk_session, - resolve_chunk_session_dir, resolve_upload_destination, - save_upload_chunk, save_upload_stream, ) from B03_FileInput.B03_FileInput_Engine_Analyze import analyze_input_metadata from B03_FileInput.B03_FileInput_Repository import ( create_input_file, get_project_storage_relative_path, - list_completed_chunk_indexes, - mark_upload_session_completed, - mark_upload_session_failed, supersede_previous_input_files, - upsert_upload_chunk, ) from B03_FileInput.B03_FileInput_Repository_Temp import ( create_temp_batch, - create_temp_upload_session, delete_temp_batch, delete_temp_batch_file, get_temp_batch, get_temp_batch_file, - get_temp_batch_file_types, - get_temp_upload_session, is_batch_required_complete, list_temp_batch_files, list_temp_batch_sessions, list_temp_batches, - mark_temp_batch_completed, - mark_temp_batch_incomplete, mark_temp_batch_linked, upsert_temp_batch_file, ) from B03_FileInput.B03_FileInput_Router_Errors import lookup_error_response +from B03_FileInput.B03_FileInput_Router_Temp_Chunks import router as chunk_router +from B03_FileInput.B03_FileInput_Router_Temp_Support import ( + _POINT_CLOUD_FILE_TYPES, + _batch_root, + _iso, + _refresh_batch_status, +) from B03_FileInput.B03_FileInput_Schema import ( - ChunkSessionCreateRequest, - ChunkSessionCreateResponse, - ChunkUploadResponse, FileUploadDescriptor, - UploadFinalizeRequest, - UploadStatusResponse, ) from B03_FileInput.B03_FileInput_Schema_Temp import ( TempBatchAttachResponse, @@ -87,7 +76,6 @@ from common_util.common_util_workflow_state import ( from config.config_db import get_db_pool from config.config_system import ( TEMP_UPLOAD_RETENTION_DAYS, - UPLOAD_CHUNK_SIZE_BYTES, UPLOAD_MAX_FILES, ) @@ -97,33 +85,8 @@ logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/temp-uploads", tags=["B03 Temp Upload"]) attach_router = APIRouter(prefix="/api/projects", tags=["B03 Temp Upload"]) -_POINT_CLOUD_FILE_TYPES = frozenset({"las", "laz"}) - - -def _total_chunks(size_bytes: int, chunk_size_bytes: int) -> int: - return max(1, (size_bytes + chunk_size_bytes - 1) // chunk_size_bytes) - - -def _iso(value: Any) -> str | None: - return value.isoformat() if value is not None and hasattr(value, "isoformat") else None - - -async def _batch_root(connection: Any, *, batch_id: str, user_id: int) -> Path: - """소유권을 확인하고 묶음 폴더를 돌려준다.""" - await get_temp_batch(connection, batch_id=batch_id, user_id=user_id) - return Path(resolve_temp_batch_path(user_id, batch_id)) - - -async def _refresh_batch_status(connection: Any, *, batch_id: str) -> bool: - """필수 파일 충족 여부에 맞춰 상태를 맞춘다. 완료 여부를 돌려준다.""" - file_types = await get_temp_batch_file_types(connection, batch_id=batch_id) - complete = is_batch_required_complete(file_types) - if complete: - await mark_temp_batch_completed(connection, batch_id=batch_id) - else: - # 파일을 지워 필수 조건이 깨진 경우 — 프로젝트 연결 대상에서 빠져야 한다. - await mark_temp_batch_incomplete(connection, batch_id=batch_id) - return complete +# 청크 업로드 엔드포인트는 파일이 700줄을 넘어 떼어냈다(2026-09-04) — 경로는 그대로다. +router.include_router(chunk_router) @router.post("", response_model=TempBatchCreateResponse) @@ -367,240 +330,6 @@ async def upload_batch_files( await upload.close() -@router.post("/{batch_id}/upload-sessions", response_model=ChunkSessionCreateResponse) -async def create_batch_upload_session( - batch_id: str, - payload: ChunkSessionCreateRequest, - session: dict[str, Any] = Depends(verify_session), -) -> ChunkSessionCreateResponse | JSONResponse: - """대용량 파일(LAS/LAZ) 청크 세션을 만든다 — 프로젝트 업로드와 같은 규칙.""" - user_id = int(session["user_id"]) - chunk_size_bytes = min(payload.chunk_size_bytes, UPLOAD_CHUNK_SIZE_BYTES) - total_chunks = _total_chunks(payload.size_bytes, chunk_size_bytes) - session_id = str(uuid4()) - pool = get_db_pool() - try: - async with pool.acquire() as connection: - await _batch_root(connection, batch_id=batch_id, user_id=user_id) - await create_temp_upload_session( - connection, - session_id=session_id, - batch_id=batch_id, - original_filename=payload.original_filename, - file_size_bytes=payload.size_bytes, - chunk_size_bytes=chunk_size_bytes, - total_chunks=total_chunks, - ) - await connection.commit() - return ChunkSessionCreateResponse( - project_id=batch_id, - upload_session_id=session_id, - original_filename=payload.original_filename, - file_size_bytes=payload.size_bytes, - chunk_size_bytes=chunk_size_bytes, - total_chunks=total_chunks, - ) - except LookupError as exc: - return lookup_error_response(exc, logger, context="B03 보관함", batch_id=batch_id) - except (OSError, ValueError) as exc: - return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) - except Exception: - logger.exception("임시 보관함 청크 세션 생성 실패: batch_id=%s", batch_id) - return JSONResponse( - status_code=500, - content={"status": "error", "message": "업로드 세션 생성 중 오류가 발생했습니다."}, - ) - - -@router.post("/{batch_id}/chunks", response_model=ChunkUploadResponse) -async def upload_batch_chunk( - batch_id: str, - session_id: str = Form(...), - chunk_index: int = Form(...), - chunk_data: UploadFile = File(...), - session: dict[str, Any] = Depends(verify_session), -) -> ChunkUploadResponse | JSONResponse: - """청크 한 조각을 보관함 묶음 폴더에 저장한다.""" - user_id = int(session["user_id"]) - pool = get_db_pool() - try: - async with pool.acquire() as connection: - batch_root = await _batch_root(connection, batch_id=batch_id, user_id=user_id) - upload_session = await get_temp_upload_session( - connection, batch_id=batch_id, session_id=session_id - ) - if chunk_index < 0 or chunk_index >= int(upload_session["total_chunks"]): - return JSONResponse( - status_code=400, - content={"status": "error", "message": "청크 인덱스가 범위를 벗어났습니다."}, - ) - session_dir = resolve_chunk_session_dir(batch_root, session_id) - chunk_path, size_bytes, chunk_hash = await save_upload_chunk( - chunk_data, - session_dir, - chunk_index, - expected_max_bytes=int(upload_session["chunk_size_bytes"]), - ) - completed_chunks = await upsert_upload_chunk( - connection, - session_id=session_id, - chunk_index=chunk_index, - chunk_hash=chunk_hash, - size_bytes=size_bytes, - stored_at=chunk_path.relative_to(batch_root).as_posix(), - ) - return ChunkUploadResponse( - upload_session_id=session_id, - chunk_index=chunk_index, - completed_chunks=completed_chunks, - total_chunks=int(upload_session["total_chunks"]), - chunk_hash=chunk_hash, - ) - except LookupError as exc: - return lookup_error_response(exc, logger, context="B03 보관함", batch_id=batch_id) - except (OSError, ValueError) as exc: - return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) - except Exception: - logger.exception("임시 보관함 청크 업로드 실패: batch_id=%s", batch_id) - return JSONResponse( - status_code=500, - content={"status": "error", "message": "청크 업로드 중 오류가 발생했습니다."}, - ) - finally: - await chunk_data.close() - - -@router.get("/{batch_id}/upload-status/{session_id}", response_model=UploadStatusResponse) -async def get_batch_upload_status( - batch_id: str, - session_id: str, - session: dict[str, Any] = Depends(verify_session), -) -> UploadStatusResponse | JSONResponse: - """이어올리기용 — 이미 올라간 청크 번호를 돌려준다.""" - user_id = int(session["user_id"]) - pool = get_db_pool() - try: - async with pool.acquire() as connection: - await get_temp_batch(connection, batch_id=batch_id, user_id=user_id) - upload_session = await get_temp_upload_session( - connection, batch_id=batch_id, session_id=session_id - ) - completed_indexes = await list_completed_chunk_indexes( - connection, session_id=session_id - ) - return UploadStatusResponse( - upload_session_id=session_id, - upload_status=str(upload_session["status"]), - original_filename=str(upload_session["original_filename"]), - file_size_bytes=int(upload_session["file_size_bytes"]), - chunk_size_bytes=int(upload_session["chunk_size_bytes"]), - total_chunks=int(upload_session["total_chunks"]), - completed_chunks=len(completed_indexes), - completed_chunk_indexes=completed_indexes, - ) - except LookupError as exc: - return lookup_error_response(exc, logger, context="B03 보관함", batch_id=batch_id) - except Exception: - logger.exception("임시 보관함 업로드 상태 조회 실패: batch_id=%s", batch_id) - return JSONResponse( - status_code=500, - content={"status": "error", "message": "업로드 상태 조회 중 오류가 발생했습니다."}, - ) - - -@router.post("/{batch_id}/finalize", response_model=TempFileUploadResponse) -async def finalize_batch_upload( - batch_id: str, - payload: UploadFinalizeRequest, - session: dict[str, Any] = Depends(verify_session), -) -> TempFileUploadResponse | JSONResponse: - """청크를 병합해 보관함에 저장하고, 필수 파일이 다 차면 완료로 올린다.""" - user_id = int(session["user_id"]) - pool = get_db_pool() - final_path: Path | None = None - try: - async with pool.acquire() as connection: - batch_root = await _batch_root(connection, batch_id=batch_id, user_id=user_id) - upload_session = await get_temp_upload_session( - connection, batch_id=batch_id, session_id=payload.session_id - ) - if int(upload_session["total_chunks"]) != payload.total_chunks: - return JSONResponse( - status_code=400, - content={"status": "error", "message": "세션 청크 개수가 일치하지 않습니다."}, - ) - completed_indexes = await list_completed_chunk_indexes( - connection, session_id=payload.session_id - ) - if completed_indexes != list(range(payload.total_chunks)): - return JSONResponse( - status_code=400, - content={"status": "error", "message": "아직 업로드되지 않은 청크가 있습니다."}, - ) - - descriptor = FileUploadDescriptor( - original_filename=str(upload_session["original_filename"]), - size_bytes=int(upload_session["file_size_bytes"]), - ) - final_path = merge_upload_chunks( - batch_root, descriptor, payload.session_id, payload.total_chunks - ) - metadata = await asyncio.to_thread(analyze_input_metadata, final_path) - relative_path = final_path.relative_to(batch_root).as_posix() - file_type = final_path.suffix.lower().lstrip(".") - crs_epsg = metadata.get("epsg") - - await connection.begin() - try: - await upsert_temp_batch_file( - connection, - batch_id=batch_id, - file_type=file_type, - original_filename=descriptor.original_filename, - relative_path=relative_path, - file_size_bytes=int(upload_session["file_size_bytes"]), - crs_epsg=int(crs_epsg) if crs_epsg is not None else None, - metadata=metadata, - ) - await mark_upload_session_completed(connection, session_id=payload.session_id) - required_complete = await _refresh_batch_status(connection, batch_id=batch_id) - await connection.commit() - except Exception: - await connection.rollback() - await mark_upload_session_failed(connection, session_id=payload.session_id) - raise - - remove_chunk_session(batch_root, payload.session_id) - return TempFileUploadResponse( - batch_id=batch_id, - files=[ - TempFileUploadResult( - batch_id=batch_id, - file_type=file_type, - original_filename=descriptor.original_filename, - relative_path=relative_path, - size_bytes=int(upload_session["file_size_bytes"]), - metadata=metadata, - ) - ], - required_complete=required_complete, - ) - except LookupError as exc: - return lookup_error_response(exc, logger, context="B03 보관함", batch_id=batch_id) - except (OSError, ValueError) as exc: - if final_path is not None: - final_path.unlink(missing_ok=True) - return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) - except Exception: - if final_path is not None: - final_path.unlink(missing_ok=True) - logger.exception("임시 보관함 병합 실패: batch_id=%s", batch_id) - return JSONResponse( - status_code=500, - content={"status": "error", "message": "업로드 최종 처리 중 오류가 발생했습니다."}, - ) - - @attach_router.post( "/{project_id}/temp-uploads/{batch_id}/attach", response_model=TempBatchAttachResponse ) diff --git a/B03_FileInput/B03_FileInput_Router_Temp_Chunks.py b/B03_FileInput/B03_FileInput_Router_Temp_Chunks.py new file mode 100644 index 00000000..20990d92 --- /dev/null +++ b/B03_FileInput/B03_FileInput_Router_Temp_Chunks.py @@ -0,0 +1,294 @@ +"""임시 보관함 청크 업로드 엔드포인트 — 세션 생성·조각 전송·진행 조회·마무리. + +라우터가 700줄을 넘어 떼어냈다(2026-09-04). 본체가 `include_router` 로 붙이므로 +여기 라우터에는 prefix 를 두지 않는다 — 두면 `/api/temp-uploads` 가 두 번 붙는다. +""" + +import asyncio +import logging +from pathlib import Path +from typing import Any +from uuid import uuid4 + +from fastapi import APIRouter, Depends, File, Form, UploadFile +from fastapi.responses import JSONResponse + +from B03_FileInput.B03_FileInput_Engine import ( + merge_upload_chunks, + remove_chunk_session, + resolve_chunk_session_dir, + save_upload_chunk, +) +from B03_FileInput.B03_FileInput_Engine_Analyze import analyze_input_metadata +from B03_FileInput.B03_FileInput_Repository import ( + list_completed_chunk_indexes, + mark_upload_session_completed, + mark_upload_session_failed, + upsert_upload_chunk, +) +from B03_FileInput.B03_FileInput_Repository_Temp import ( + create_temp_upload_session, + get_temp_batch, + get_temp_upload_session, + upsert_temp_batch_file, +) +from B03_FileInput.B03_FileInput_Router_Errors import lookup_error_response +from B03_FileInput.B03_FileInput_Router_Temp_Support import ( + _batch_root, + _refresh_batch_status, + _total_chunks, +) +from B03_FileInput.B03_FileInput_Schema import ( + ChunkSessionCreateRequest, + ChunkSessionCreateResponse, + ChunkUploadResponse, + FileUploadDescriptor, + UploadFinalizeRequest, + UploadStatusResponse, +) +from B03_FileInput.B03_FileInput_Schema_Temp import ( + TempFileUploadResponse, + TempFileUploadResult, +) +from common_util.common_util_auth import verify_session +from config.config_db import get_db_pool +from config.config_system import ( + UPLOAD_CHUNK_SIZE_BYTES, +) + +logger = logging.getLogger(__name__) +router = APIRouter(tags=["B03 Temp Upload"]) + + +@router.post("/{batch_id}/upload-sessions", response_model=ChunkSessionCreateResponse) +async def create_batch_upload_session( + batch_id: str, + payload: ChunkSessionCreateRequest, + session: dict[str, Any] = Depends(verify_session), +) -> ChunkSessionCreateResponse | JSONResponse: + """대용량 파일(LAS/LAZ) 청크 세션을 만든다 — 프로젝트 업로드와 같은 규칙.""" + user_id = int(session["user_id"]) + chunk_size_bytes = min(payload.chunk_size_bytes, UPLOAD_CHUNK_SIZE_BYTES) + total_chunks = _total_chunks(payload.size_bytes, chunk_size_bytes) + session_id = str(uuid4()) + pool = get_db_pool() + try: + async with pool.acquire() as connection: + await _batch_root(connection, batch_id=batch_id, user_id=user_id) + await create_temp_upload_session( + connection, + session_id=session_id, + batch_id=batch_id, + original_filename=payload.original_filename, + file_size_bytes=payload.size_bytes, + chunk_size_bytes=chunk_size_bytes, + total_chunks=total_chunks, + ) + await connection.commit() + return ChunkSessionCreateResponse( + project_id=batch_id, + upload_session_id=session_id, + original_filename=payload.original_filename, + file_size_bytes=payload.size_bytes, + chunk_size_bytes=chunk_size_bytes, + total_chunks=total_chunks, + ) + except LookupError as exc: + return lookup_error_response(exc, logger, context="B03 보관함", batch_id=batch_id) + except (OSError, ValueError) as exc: + return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) + except Exception: + logger.exception("임시 보관함 청크 세션 생성 실패: batch_id=%s", batch_id) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "업로드 세션 생성 중 오류가 발생했습니다."}, + ) + + +@router.post("/{batch_id}/chunks", response_model=ChunkUploadResponse) +async def upload_batch_chunk( + batch_id: str, + session_id: str = Form(...), + chunk_index: int = Form(...), + chunk_data: UploadFile = File(...), + session: dict[str, Any] = Depends(verify_session), +) -> ChunkUploadResponse | JSONResponse: + """청크 한 조각을 보관함 묶음 폴더에 저장한다.""" + user_id = int(session["user_id"]) + pool = get_db_pool() + try: + async with pool.acquire() as connection: + batch_root = await _batch_root(connection, batch_id=batch_id, user_id=user_id) + upload_session = await get_temp_upload_session( + connection, batch_id=batch_id, session_id=session_id + ) + if chunk_index < 0 or chunk_index >= int(upload_session["total_chunks"]): + return JSONResponse( + status_code=400, + content={"status": "error", "message": "청크 인덱스가 범위를 벗어났습니다."}, + ) + session_dir = resolve_chunk_session_dir(batch_root, session_id) + chunk_path, size_bytes, chunk_hash = await save_upload_chunk( + chunk_data, + session_dir, + chunk_index, + expected_max_bytes=int(upload_session["chunk_size_bytes"]), + ) + completed_chunks = await upsert_upload_chunk( + connection, + session_id=session_id, + chunk_index=chunk_index, + chunk_hash=chunk_hash, + size_bytes=size_bytes, + stored_at=chunk_path.relative_to(batch_root).as_posix(), + ) + return ChunkUploadResponse( + upload_session_id=session_id, + chunk_index=chunk_index, + completed_chunks=completed_chunks, + total_chunks=int(upload_session["total_chunks"]), + chunk_hash=chunk_hash, + ) + except LookupError as exc: + return lookup_error_response(exc, logger, context="B03 보관함", batch_id=batch_id) + except (OSError, ValueError) as exc: + return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) + except Exception: + logger.exception("임시 보관함 청크 업로드 실패: batch_id=%s", batch_id) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "청크 업로드 중 오류가 발생했습니다."}, + ) + finally: + await chunk_data.close() + + +@router.get("/{batch_id}/upload-status/{session_id}", response_model=UploadStatusResponse) +async def get_batch_upload_status( + batch_id: str, + session_id: str, + session: dict[str, Any] = Depends(verify_session), +) -> UploadStatusResponse | JSONResponse: + """이어올리기용 — 이미 올라간 청크 번호를 돌려준다.""" + user_id = int(session["user_id"]) + pool = get_db_pool() + try: + async with pool.acquire() as connection: + await get_temp_batch(connection, batch_id=batch_id, user_id=user_id) + upload_session = await get_temp_upload_session( + connection, batch_id=batch_id, session_id=session_id + ) + completed_indexes = await list_completed_chunk_indexes( + connection, session_id=session_id + ) + return UploadStatusResponse( + upload_session_id=session_id, + upload_status=str(upload_session["status"]), + original_filename=str(upload_session["original_filename"]), + file_size_bytes=int(upload_session["file_size_bytes"]), + chunk_size_bytes=int(upload_session["chunk_size_bytes"]), + total_chunks=int(upload_session["total_chunks"]), + completed_chunks=len(completed_indexes), + completed_chunk_indexes=completed_indexes, + ) + except LookupError as exc: + return lookup_error_response(exc, logger, context="B03 보관함", batch_id=batch_id) + except Exception: + logger.exception("임시 보관함 업로드 상태 조회 실패: batch_id=%s", batch_id) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "업로드 상태 조회 중 오류가 발생했습니다."}, + ) + + +@router.post("/{batch_id}/finalize", response_model=TempFileUploadResponse) +async def finalize_batch_upload( + batch_id: str, + payload: UploadFinalizeRequest, + session: dict[str, Any] = Depends(verify_session), +) -> TempFileUploadResponse | JSONResponse: + """청크를 병합해 보관함에 저장하고, 필수 파일이 다 차면 완료로 올린다.""" + user_id = int(session["user_id"]) + pool = get_db_pool() + final_path: Path | None = None + try: + async with pool.acquire() as connection: + batch_root = await _batch_root(connection, batch_id=batch_id, user_id=user_id) + upload_session = await get_temp_upload_session( + connection, batch_id=batch_id, session_id=payload.session_id + ) + if int(upload_session["total_chunks"]) != payload.total_chunks: + return JSONResponse( + status_code=400, + content={"status": "error", "message": "세션 청크 개수가 일치하지 않습니다."}, + ) + completed_indexes = await list_completed_chunk_indexes( + connection, session_id=payload.session_id + ) + if completed_indexes != list(range(payload.total_chunks)): + return JSONResponse( + status_code=400, + content={"status": "error", "message": "아직 업로드되지 않은 청크가 있습니다."}, + ) + + descriptor = FileUploadDescriptor( + original_filename=str(upload_session["original_filename"]), + size_bytes=int(upload_session["file_size_bytes"]), + ) + final_path = merge_upload_chunks( + batch_root, descriptor, payload.session_id, payload.total_chunks + ) + metadata = await asyncio.to_thread(analyze_input_metadata, final_path) + relative_path = final_path.relative_to(batch_root).as_posix() + file_type = final_path.suffix.lower().lstrip(".") + crs_epsg = metadata.get("epsg") + + await connection.begin() + try: + await upsert_temp_batch_file( + connection, + batch_id=batch_id, + file_type=file_type, + original_filename=descriptor.original_filename, + relative_path=relative_path, + file_size_bytes=int(upload_session["file_size_bytes"]), + crs_epsg=int(crs_epsg) if crs_epsg is not None else None, + metadata=metadata, + ) + await mark_upload_session_completed(connection, session_id=payload.session_id) + required_complete = await _refresh_batch_status(connection, batch_id=batch_id) + await connection.commit() + except Exception: + await connection.rollback() + await mark_upload_session_failed(connection, session_id=payload.session_id) + raise + + remove_chunk_session(batch_root, payload.session_id) + return TempFileUploadResponse( + batch_id=batch_id, + files=[ + TempFileUploadResult( + batch_id=batch_id, + file_type=file_type, + original_filename=descriptor.original_filename, + relative_path=relative_path, + size_bytes=int(upload_session["file_size_bytes"]), + metadata=metadata, + ) + ], + required_complete=required_complete, + ) + except LookupError as exc: + return lookup_error_response(exc, logger, context="B03 보관함", batch_id=batch_id) + except (OSError, ValueError) as exc: + if final_path is not None: + final_path.unlink(missing_ok=True) + return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) + except Exception: + if final_path is not None: + final_path.unlink(missing_ok=True) + logger.exception("임시 보관함 병합 실패: batch_id=%s", batch_id) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "업로드 최종 처리 중 오류가 발생했습니다."}, + ) diff --git a/B03_FileInput/B03_FileInput_Router_Temp_Support.py b/B03_FileInput/B03_FileInput_Router_Temp_Support.py new file mode 100644 index 00000000..30e7477a --- /dev/null +++ b/B03_FileInput/B03_FileInput_Router_Temp_Support.py @@ -0,0 +1,46 @@ +"""임시 보관함 라우터 보조 — 조각 수 계산·묶음 폴더 확인·상태 갱신. + +라우터가 700줄을 넘어 떼어냈다(2026-09-04). 본체와 청크 모듈이 함께 쓰는 것만 둔다. +""" + +from pathlib import Path +from typing import Any + +from B03_FileInput.B03_FileInput_Repository_Temp import ( + get_temp_batch, + get_temp_batch_file_types, + is_batch_required_complete, + mark_temp_batch_completed, + mark_temp_batch_incomplete, +) +from common_util.common_util_storage import ( + resolve_temp_batch_path, +) + +_POINT_CLOUD_FILE_TYPES = frozenset({"las", "laz"}) + + +def _total_chunks(size_bytes: int, chunk_size_bytes: int) -> int: + return max(1, (size_bytes + chunk_size_bytes - 1) // chunk_size_bytes) + + +def _iso(value: Any) -> str | None: + return value.isoformat() if value is not None and hasattr(value, "isoformat") else None + + +async def _batch_root(connection: Any, *, batch_id: str, user_id: int) -> Path: + """소유권을 확인하고 묶음 폴더를 돌려준다.""" + await get_temp_batch(connection, batch_id=batch_id, user_id=user_id) + return Path(resolve_temp_batch_path(user_id, batch_id)) + + +async def _refresh_batch_status(connection: Any, *, batch_id: str) -> bool: + """필수 파일 충족 여부에 맞춰 상태를 맞춘다. 완료 여부를 돌려준다.""" + file_types = await get_temp_batch_file_types(connection, batch_id=batch_id) + complete = is_batch_required_complete(file_types) + if complete: + await mark_temp_batch_completed(connection, batch_id=batch_id) + else: + # 파일을 지워 필수 조건이 깨진 경우 — 프로젝트 연결 대상에서 빠져야 한다. + await mark_temp_batch_incomplete(connection, batch_id=batch_id) + return complete