290 lines
11 KiB
Python
290 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.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,
|
|
)
|
|
|
|
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,
|
|
)
|