from __future__ import annotations from datetime import UTC, datetime import hashlib from fastapi import ( APIRouter, Depends, File as FastAPIFile, Header, HTTPException, UploadFile, status, ) from sqlalchemy.orm import Session from govoplan_core.core.events import ( EventActorRef, EventObjectRef, EventTenantRef, PlatformEvent, emit_platform_event, ) from govoplan_core.core.institutional import EvidenceReference from govoplan_core.db.session import get_session from govoplan_files.backend.db.models import FileFormEvidenceGrant from govoplan_files.backend.route_support import _read_limited_upload from govoplan_files.backend.storage.common import FileStorageError from govoplan_files.backend.storage.files import create_file_asset router = APIRouter(prefix="/files", tags=["files"]) @router.post( "/form-evidence/upload", response_model=dict[str, object], status_code=status.HTTP_201_CREATED, ) def upload_form_evidence( file: UploadFile = FastAPIFile(...), x_form_evidence_token: str = Header(alias="X-Form-Evidence-Token"), session: Session = Depends(get_session), ) -> dict[str, object]: now = datetime.now(UTC) try: grant = ( session.query(FileFormEvidenceGrant) .filter( FileFormEvidenceGrant.token_sha256 == _token_sha256(x_form_evidence_token) ) .with_for_update() .one_or_none() ) if grant is None or grant.status != "issued" or _aware(grant.expires_at) <= now: raise _upload_unavailable() content_type = ( str(file.content_type or "application/octet-stream") .split(";", 1)[0] .strip() .casefold() ) if grant.allowed_content_types and content_type not in set( grant.allowed_content_types ): raise HTTPException( status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, detail="This Form does not accept the uploaded document type.", ) data = _read_limited_upload(file, max_bytes=grant.max_size_bytes) stored = create_file_asset( session, tenant_id=grant.tenant_id, owner_type="user", owner_id=grant.custodian_user_id, user_id=grant.custodian_user_id, filename=file.filename or "form-attachment", data=data, folder=f"Form submissions/{grant.form_instance_id}", content_type=content_type, description="Managed attachment captured through Forms Runtime.", metadata={ "form_evidence": { "grant_id": grant.id, "form_instance_id": grant.form_instance_id, "form_definition_id": grant.form_definition_id, "form_definition_revision": grant.form_definition_revision, "purpose": grant.purpose, }, "source_provenance": { "source_type": "form_evidence", "connector_id": "forms_runtime.evidence.files", "provider": "forms_runtime", "external_id": grant.id, "revision": grant.form_definition_revision, }, }, conflict_strategy="rename", is_admin=True, ) grant.status = "uploaded" grant.file_asset_id = stored.asset.id grant.file_version_id = stored.version.id grant.uploaded_at = now session.add(grant) evidence = EvidenceReference( kind="document", owner_module="files", evidence_id=stored.asset.id, tenant_id=grant.tenant_id, version=stored.version.id, checksum=stored.version.checksum_sha256, source_ref=(f"form_submission:{grant.form_instance_id}:grant:{grant.id}"), responsible_actor_ref=f"user:{grant.custodian_user_id}", captured_at=now, ) emit_platform_event( session, PlatformEvent( type="files.form_evidence.uploaded", module_id="files", payload={ "grant_id": grant.id, "form_instance_id": grant.form_instance_id, "file_asset_id": stored.asset.id, "file_version_id": stored.version.id, "checksum_sha256": stored.version.checksum_sha256, "size_bytes": stored.version.size_bytes, }, occurred_at=now, actor=EventActorRef(type="user", id=grant.custodian_user_id), tenant=EventTenantRef(id=grant.tenant_id), resource=EventObjectRef(type="file", id=stored.asset.id), classification="confidential", ), ) session.commit() except HTTPException: session.rollback() raise except (FileStorageError, ValueError) as exc: session.rollback() raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc), ) from exc return { "grant_id": grant.id, "evidence": evidence.to_dict(include_inspection=True), } def _token_sha256(token: str) -> str: clean = str(token or "").strip() if len(clean) < 32: raise _upload_unavailable() return hashlib.sha256(clean.encode("utf-8")).hexdigest() def _upload_unavailable() -> HTTPException: return HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="The Form evidence upload is unavailable.", ) def _aware(value: datetime) -> datetime: return value if value.tzinfo is not None else value.replace(tzinfo=UTC) __all__ = ["router", "upload_form_evidence"]