Initial commit: svg backend
This commit is contained in:
261
backend/app/api/routes/uploads.py
Normal file
261
backend/app/api/routes/uploads.py
Normal file
@@ -0,0 +1,261 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user