Anhänge-API
Status: Entwurf · Spec-Kandidat: ja
Zweck
REST-Endpunkte für Datei-Anhänge (DDM_ATTACHMENT). Verhalten (Upload-Phasen, Virus-Scan, Cleanup) liegt in Anhänge-Verhalten.
Endpunkt-Übersicht
| Methode + Pfad | Zweck | Permission |
|---|---|---|
POST /attachments:initiate | Upload-Session anlegen, Pre-Signed-PUT-URL erhalten | update auf Entity / manage_metadata falls entity_id null |
POST /attachments:commit | Upload abschließen, DDM_ATTACHMENT-Zeile anlegen | dito |
GET /entities/{id}/attachments | Anhänge einer Entity listen (paginiert, gefiltert nach logical_key, is_current) | read auf Entity |
GET /attachments/{id} | Pre-Signed-GET-URL (302-Redirect) | read |
GET /attachments/{id}/content | Direct-Stream durch MDM (optional, große Dateien vermeiden) | read |
GET /attachments/{id}/metadata | Reine Metadaten (JSON) | read |
DELETE /attachments/{id} | Soft-Delete | update auf Entity |
POST /attachments/{id}:restore | Soft-Delete rückgängig | update |
POST /attachments/{id}:purge | Hard-Delete (Storage + Zeile) | manage_metadata |
Alle Pfade sind tenant-scoped — tenant_id aus dem Auth-Kontext, kein Pfad-Parameter.
Initiate
POST /attachments:initiateContent-Type: application/json{ "entity_id": "c-1001-uuid-...", // optional "logical_key": "vertragsscan", // optional, regex ^[a-z][a-z0-9_]*$ "filename": "vertrag_2026.pdf", "mime": "application/pdf", "size_bytes": 1248192}200 OK{ "upload_session_id": "ups-...", "presigned_put_url": "https://minio.acme.example.com/...", "expires_at": "2026-04-27T13:05:00Z", "storage_uri": "s3://mdm-acme/acme/<uuid>", "max_size_bytes": 104857600}Fehlerfälle:
403— Permission-Defizit (nach den Standard-403/404-Regeln, siehe Autorisierung).413 Payload Too Large—size_bytesüberschreitet Tenant- oder Plattform-Limit.415 Unsupported Media Type—mimenicht in Tenant-Whitelist.400— Format-Verletzung vonlogical_key/filename.
Hinweis: Der Server vergibt bereits hier die zukünftige attachment.id und schreibt sie in die AA_UPLOAD_SESSION-Zeile. Damit ist die Storage-URI deterministisch — ein nachgelagertes commit kann sie nicht beliebig ändern.
Direkter Upload
Client PUTtet die Bytes zur presigned_put_url. Erfolg = 200 OK vom Storage. MDM ist nicht beteiligt. Etag aus Storage-Antwort sollte der Client behalten, ist aber nicht Pflicht — commit validiert über separate Hash-Übermittlung.
Commit
POST /attachments:commit{ "upload_session_id": "ups-...", "sha256": "9f86d0...", // hex, 64 Zeichen "size_bytes": 1248192 // optional, Server validiert}201 Created{ "attachment": { "id": "...", "tenant_id": "...", "entity_id": "...", "logical_key": "vertragsscan", "version_no": 3, "is_current": true, "filename": "vertrag_2026.pdf", "mime": "application/pdf", "size_bytes": 1248192, "sha256": "9f86d0...", "storage_backend": "minio", "virus_scan_status": "pending", "created_at": "2026-04-27T13:01:42Z", ... }}Fehlerfälle:
404—upload_session_idunbekannt oder abgelaufen.409 Conflict— Session bereits committed; oder Storage-Object fehlt; odersize_bytesweicht von Phase-1-Wert ab.410 Gone— Session expired.
Listing
GET /entities/{id}/attachments?logical_key=vertragsscan&include_history=true&page_size=50→ 200 OK{ "items": [ ... DDM_ATTACHMENT objects ... ], "next_cursor": "..."}Default-Filter: deleted_at IS NULL und is_current=true (nur jüngste Slot-Version je logical_key). include_history=true zeigt alle Versionen. include_deleted=true zeigt Soft-Deleted (Steward-Sicht, manage_metadata-only).
Download
GET /attachments/{id}→ 302 FoundLocation: https://minio.acme.example.com/...?X-Amz-Signature=...Headers Content-Disposition und Content-Type sind im Pre-Signed-Response signiert.
Spezial-Stati:
423 Locked—virus_scan_status='pending'oderfailed. Body:{ "error": "virus_scan_pending", "scan_status": "..." }.403/404nach Standardregel.410 Gone— Storage-Object fehlt (Cleanup-Job ist gelaufen, Restore ist nicht mehr möglich).
GET /attachments/{id}/content liefert die Bytes direkt aus dem MDM (Streaming-Proxy). Default deaktiviert für Dateien > 5 MB (Konfig).
Delete / Restore / Purge
DELETE /attachments/{id}→ 204 No Content // soft-delete
POST /attachments/{id}:restore→ 200 OK { "attachment": { ... } }
POST /attachments/{id}:purge{ "reason": "GDPR Erasure Request #4711" }→ 204 No Content // hard-delete + Storage-Object wegpurge schreibt reason in AA_AUDIT_LOG. Versuch auf nicht-soft-deleted: 409 (zwingt zum bewussten Soft-Delete-First, außer per Query-Param force=true für Datenschutz-Notfälle).
Idempotenz
initiate: optionaler HeaderIdempotency-Key. Bei Wiederholung mit gleichem Key + gleichen Feldern → dieselbe Session zurückgegeben.commit: idempotent überupload_session_id— zweiter Commit liefert die existierendeDDM_ATTACHMENT-Zeile statt Konflikt.
Webhook / Event
Outbox-Events nach DDM_ATTACHMENT-Mutation:
attachment.uploaded— neue Zeile, vor Scan-Outcome.attachment.scanned—virus_scan_statusfinal (clean/infected/skipped/failed).attachment.deleted— Soft-Delete.attachment.restored— Restore.attachment.purged— Hard-Delete.
payload enthält attachment_id, entity_id (falls gesetzt), tenant_id, logical_key, version_no, virus_scan_status. Externe Konsumenten lernen so über den Lebenszyklus, ohne MDM-DB lesen zu müssen.