Files
svg-backend/backend/app/api/routes/uploads.py

281 lines
11 KiB
Python

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_artifacts import create_scheme_artifact
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.schemes import create_scheme_from_upload_with_initial_version
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, scheme_version_id = await create_scheme_from_upload_with_initial_version(
source_upload_id=upload_id,
name=Path(filename).stem or filename,
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,
)
await create_scheme_artifact(
scheme_id=scheme_id,
scheme_version_id=scheme_version_id,
artifact_type="sanitized_svg",
artifact_variant="source",
storage_path=sanitized_storage_path,
status="ready",
)
await create_scheme_artifact(
scheme_id=scheme_id,
scheme_version_id=scheme_version_id,
artifact_type="normalized_json",
artifact_variant="default",
storage_path=normalized_storage_path,
status="ready",
)
if display_svg_storage_path:
await create_scheme_artifact(
scheme_id=scheme_id,
scheme_version_id=scheme_version_id,
artifact_type="display_svg",
artifact_variant=settings.svg_display_mode,
storage_path=display_svg_storage_path,
status="ready",
meta_json=display_meta,
)
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,
)