import json import logging from datetime import datetime, timezone from pathlib import Path from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status from app.core.config import settings from app.repositories.scheme_groups import replace_scheme_version_groups from app.repositories.scheme_seats import replace_scheme_version_seats from app.repositories.scheme_sectors import replace_scheme_version_sectors from app.repositories.scheme_versions import create_initial_scheme_version from app.repositories.schemes import create_scheme_from_upload from app.repositories.uploads import ( count_upload_records, create_upload_record, get_upload_record_by_upload_id, list_upload_records, ) from app.schemas.upload import UploadResponse from app.schemas.upload_registry import UploadDetailResponse, UploadListItem, UploadListResponse from app.security.auth import require_api_key from app.services.normalized_reader import read_normalized_payload_from_path from app.services.storage import ( load_normalized_json, save_display_svg, save_normalized_json, save_original_svg, save_sanitized_svg, ) from app.services.svg_display_processor import generate_display_svg from app.services.svg_inspector import inspect_svg_bytes from app.services.svg_normalizer import normalize_svg_bytes_to_json from app.services.svg_sanitizer import sanitize_svg_bytes router = APIRouter() logger = logging.getLogger(__name__) @router.get(f"{settings.api_v1_prefix}/uploads", response_model=UploadListResponse) async def get_uploads( limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), role: str = Depends(require_api_key), ): rows = await list_upload_records(limit=limit, offset=offset) total = await count_upload_records() items = [ UploadListItem( upload_id=row.upload_id, original_filename=row.original_filename, content_type=row.content_type, size_bytes=row.size_bytes, element_count=row.element_count, removed_elements_count=row.removed_elements_count, removed_attributes_count=row.removed_attributes_count, normalized_elements_count=row.normalized_elements_count, normalized_seats_count=row.normalized_seats_count, normalized_groups_count=row.normalized_groups_count, normalized_sectors_count=row.normalized_sectors_count, original_storage_path=row.original_storage_path, sanitized_storage_path=row.sanitized_storage_path, normalized_storage_path=row.normalized_storage_path, processing_status=row.processing_status, created_at=row.created_at.isoformat(), ) for row in rows ] return UploadListResponse(items=items, total=total) @router.get(f"{settings.api_v1_prefix}/uploads/{{upload_id}}", response_model=UploadDetailResponse) async def get_upload(upload_id: str, role: str = Depends(require_api_key)): row = await get_upload_record_by_upload_id(upload_id) return UploadDetailResponse( upload_id=row.upload_id, original_filename=row.original_filename, content_type=row.content_type, size_bytes=row.size_bytes, element_count=row.element_count, removed_elements_count=row.removed_elements_count, removed_attributes_count=row.removed_attributes_count, normalized_elements_count=row.normalized_elements_count, normalized_seats_count=row.normalized_seats_count, normalized_groups_count=row.normalized_groups_count, normalized_sectors_count=row.normalized_sectors_count, original_storage_path=row.original_storage_path, sanitized_storage_path=row.sanitized_storage_path, normalized_storage_path=row.normalized_storage_path, processing_status=row.processing_status, created_at=row.created_at.isoformat(), ) @router.get(f"{settings.api_v1_prefix}/uploads/{{upload_id}}/normalized") async def get_normalized(upload_id: str, role: str = Depends(require_api_key)): payload = load_normalized_json(upload_id) return json.loads(payload) @router.post(f"{settings.api_v1_prefix}/schemes/upload", response_model=UploadResponse) async def upload_scheme_svg( file: UploadFile = File(...), role: str = Depends(require_api_key), ): filename = file.filename or "" suffix = Path(filename).suffix.lower() content_type = (file.content_type or "").lower() if suffix != ".svg" and content_type != "image/svg+xml": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Only SVG files are allowed", ) content = await file.read() size_bytes = len(content) if size_bytes == 0: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Uploaded file is empty", ) if size_bytes > settings.svg_max_file_size_bytes: raise HTTPException( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="SVG file exceeds configured size limit", ) element_count = inspect_svg_bytes(content) sanitized_content, removed_elements_count, removed_attributes_count = sanitize_svg_bytes(content) normalized_json, normalized_payload = normalize_svg_bytes_to_json(sanitized_content) display_svg_storage_path = None display_svg_status = "pending" display_svg_generated_at = None if settings.svg_display_enabled: try: display_content, display_meta = generate_display_svg( sanitized_content, settings.svg_display_mode, ) logger.info( "display_svg.upload generated mode=%s view_box=%s width=%s height=%s", settings.svg_display_mode, display_meta.get("view_box"), display_meta.get("width"), display_meta.get("height"), ) display_svg_status = "ready" except Exception: logger.exception("display_svg.upload failed filename=%s", filename) display_content = None display_svg_status = "failed" else: display_content = None display_svg_status = "failed" upload_id, original_storage_path = save_original_svg(filename=filename, content=content) sanitized_storage_path = save_sanitized_svg( upload_id=upload_id, filename=filename, content=sanitized_content, ) normalized_storage_path = save_normalized_json( upload_id=upload_id, filename=filename, content=normalized_json, ) if display_content is not None: display_svg_storage_path = save_display_svg( upload_id=upload_id, filename=filename, content=display_content, ) display_svg_generated_at = datetime.now(timezone.utc) summary = normalized_payload["summary"] await create_upload_record( upload_id=upload_id, original_filename=filename, content_type=content_type, size_bytes=size_bytes, element_count=element_count, removed_elements_count=removed_elements_count, removed_attributes_count=removed_attributes_count, normalized_elements_count=summary["elements_count"], normalized_seats_count=summary["seats_count"], normalized_groups_count=summary["groups_count"], normalized_sectors_count=summary["sectors_count"], original_storage_path=original_storage_path, sanitized_storage_path=sanitized_storage_path, normalized_storage_path=normalized_storage_path, processing_status="completed", ) scheme_id = await create_scheme_from_upload( source_upload_id=upload_id, name=Path(filename).stem or filename, normalized_elements_count=summary["elements_count"], normalized_seats_count=summary["seats_count"], normalized_groups_count=summary["groups_count"], normalized_sectors_count=summary["sectors_count"], ) scheme_version_id = await create_initial_scheme_version( scheme_id=scheme_id, normalized_storage_path=normalized_storage_path, normalized_elements_count=summary["elements_count"], normalized_seats_count=summary["seats_count"], normalized_groups_count=summary["groups_count"], normalized_sectors_count=summary["sectors_count"], display_svg_storage_path=display_svg_storage_path, display_svg_status=display_svg_status, display_svg_generated_at=display_svg_generated_at, ) normalized_payload_from_file = read_normalized_payload_from_path(normalized_storage_path) await replace_scheme_version_sectors( scheme_id=scheme_id, scheme_version_id=scheme_version_id, sectors=normalized_payload_from_file.get("sectors", []), ) await replace_scheme_version_groups( scheme_id=scheme_id, scheme_version_id=scheme_version_id, groups=normalized_payload_from_file.get("groups", []), ) await replace_scheme_version_seats( scheme_id=scheme_id, scheme_version_id=scheme_version_id, seats=normalized_payload_from_file.get("seats", []), ) return UploadResponse( upload_id=upload_id, filename=filename, content_type=content_type, size_bytes=size_bytes, element_count=element_count, removed_elements_count=removed_elements_count, removed_attributes_count=removed_attributes_count, normalized_elements_count=summary["elements_count"], normalized_seats_count=summary["seats_count"], normalized_groups_count=summary["groups_count"], normalized_sectors_count=summary["sectors_count"], svg_max_file_size_bytes=settings.svg_max_file_size_bytes, svg_max_elements=settings.svg_max_elements, original_storage_path=original_storage_path, sanitized_storage_path=sanitized_storage_path, normalized_storage_path=normalized_storage_path, accepted=True, )