173 lines
5.8 KiB
Python
173 lines
5.8 KiB
Python
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"]
|