"""仅上传调用方显式提供的单个 PNG 的窄 EvidenceSink。""" from __future__ import annotations from cmbuyer_client.core.errors import AmbiguousRemoteError, ProtocolRemoteError, ValidationError from cmbuyer_client.core.models import AssetReceipt, DeviceCredentials, EvidenceUpload from cmbuyer_client.core.validation import rfc3339_z_nanoseconds from .http_transport import HttpTransport from .wire import SMALL_RESPONSE_LIMIT, classify_bodyless_error, common_headers, parse_json_response class HttpEvidenceSink: def __init__(self, transport: HttpTransport) -> None: self._transport = transport def upload(self, credentials: DeviceCredentials, evidence: EvidenceUpload) -> AssetReceipt: boundary = "cmbuyer-" + evidence.upload_key.replace("-", "") marker = ("--" + boundary).encode("ascii") if marker in evidence.content: raise ProtocolRemoteError("multipart_boundary_collision") body = _multipart_body(boundary, evidence) response = self._transport.request( "POST", f"/api/v1/tasks/{evidence.task_id}/evidence", common_headers( credentials.device_id, credentials.token.value, "multipart/form-data; boundary=" + boundary, ), body, response_limit=SMALL_RESPONSE_LIMIT, ) if response.status not in (200, 201): classify_bodyless_error(response) try: receipt = AssetReceipt.from_wire(parse_json_response(response, maximum=SMALL_RESPONSE_LIMIT)) except ValidationError as error: raise AmbiguousRemoteError("invalid_evidence_success_response") from error if ( receipt.task_id != evidence.task_id or receipt.attempt_id != evidence.attempt_id or receipt.kind != evidence.kind or receipt.privacy_tier != evidence.privacy_tier or receipt.sha256 != evidence.sha256 or receipt.byte_size != len(evidence.content) or receipt.width_px != evidence.width_px or receipt.height_px != evidence.height_px or rfc3339_z_nanoseconds(receipt.captured_at) != rfc3339_z_nanoseconds(evidence.captured_at) ): raise AmbiguousRemoteError("evidence_response_mismatch") return receipt def _multipart_body(boundary: str, evidence: EvidenceUpload) -> bytes: chunks: list[bytes] = [] def add_field(name: str, value: str) -> None: chunks.extend( ( f"--{boundary}\r\n".encode("ascii"), f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode("ascii"), value.encode("utf-8"), b"\r\n", ) ) add_field("upload_key", evidence.upload_key) add_field("attempt_id", evidence.attempt_id) add_field("kind", evidence.kind) add_field("privacy_tier", evidence.privacy_tier) add_field("sha256", evidence.sha256) add_field("captured_at", evidence.captured_at) chunks.extend( ( f"--{boundary}\r\n".encode("ascii"), b'Content-Disposition: form-data; name="file"; filename="evidence.png"\r\n', b"Content-Type: image/png\r\n\r\n", evidence.content, b"\r\n", f"--{boundary}--\r\n".encode("ascii"), ) ) return b"".join(chunks)