85 lines
3.4 KiB
Python
85 lines
3.4 KiB
Python
"""仅上传调用方显式提供的单个 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)
|