OBS/Kostenpflichtige Module/RESTServer/Beispiel6
- A Preise aktualisieren
- C Personen übertragen
- E Kategorien verwalten
- G Kataloge verwalten
- I Merkliste übertragen
- K Varianten übertragen
- L Artikelvarianten übertragen
- M Referenzarten übertragen
- N Lagerbestände verwalten
- U Bestellungen einlesen
- V leere Passworte füllen
- W Update-Informationen zurücksetzen
- X Konfiguration
- Z Protokoll
Beispiel 6: Datei-Upload und Ablage
Dieses Beispiel zeigt einen Endpunkt, der Dateien entgegennimmt: Ein Aussendienst-Mitarbeiter fotografiert vor Ort ein Gerät, die App lädt das Foto zum Auftrag hoch, der Server legt es ab und verknüpft es mit dem Vorgang.
Der Ablauf ist derselbe für Belege aus einem Scanner, Unterschriften-Bilder oder Prüfprotokolle als PDF. Behandelt werden beide Übertragungsarten - der einfache Upload für kleine Dateien und der fortsetzbare Upload für grosse.
Einrichtung in OBS
- Server-Profil: Standard-TLS-Profil Public-API (Port 443)
- Zugang: Mobile-App, API-Key zufällig generiert, JWT aktiv
- Endpunkt: /orders/{uid}/photos, Profil Public-API
- Datei-Upload auf Ja setzen (
re_upload = 1) - Max. Upload-Grösse auf 20 (MB) setzen
- Datei-Upload auf Ja setzen (
- Berechtigung: Zugang Mobile-App für den Endpunkt freischalten
415 ab und das Skript läuft gar nicht erst an. Das ist die häufigste Ursache,
wenn ein Upload "ohne erkennbaren Grund" scheitert.Was der Server erledigt, bevor das Skript läuft
Der Server nimmt die Datei entgegen, prüft die Grösse, legt sie in einem temporären Verzeichnis ab und übergibt dem Skript drei Parameter:
| Parameter | Inhalt |
|---|---|
| _OBS_UPLOAD_PATH | Vollständiger Pfad zur temporären Datei |
| _OBS_UPLOAD_NAME | Originaldateiname, wie ihn der Client gesendet hat |
| _OBS_UPLOAD_CONTENTTYPE | Content-Type der Datei |
Zwei Dinge sind beim Schreiben des Skripts wichtig:
oBodyist bei Uploads immernil. Der Request-Body ist die Datei, kein JSON. Zusatzangaben (Kategorie, Bemerkung) müssen deshalb als Query-Parameter mitkommen, nicht im Body.- Die temporäre Datei bleibt liegen, wenn das Skript sie nicht übernimmt. Der Server räumt sie nicht selbst weg.
Endpunkt-Skript /orders/{uid}/photos
Das Skript prüft den Dateityp, baut einen Zielpfad aus Auftrag und Zeitstempel, verschiebt die Datei dorthin und schreibt einen Verweis in die Datenbank.
function Post(oParams: TStrings; oBody: TJSONObject): string;
const ZIEL_BASIS = 'D:\OBS\Daten\Auftragsfotos\';
var oRes, oErr, oHdr: TJSONObject;
cUid : string;
cTmpPfad : string;
cOrigName: string;
cTyp : string;
cEndung : string;
cKategorie: string;
cZielDir : string;
cZielName: string;
cZielPfad: string;
qNeu : TqSQL;
begin
oRes := TJSONObject.Create();
try
cUid := oParams.Values['_OBS_PATH_uid'];
cTmpPfad := oParams.Values['_OBS_UPLOAD_PATH'];
cOrigName := oParams.Values['_OBS_UPLOAD_NAME'];
cTyp := oParams.Values['_OBS_UPLOAD_CONTENTTYPE'];
cKategorie := oParams.Values['kategorie']; // Query-Parameter, nicht Body
// 1) Gehoert der Auftrag zum Mandanten aus dem Token?
if (not AuftragGehoertZuMandant(oParams.Values['_OBS_JWT_CLAIM_tenant'], cUid)) then begin
oErr := TJSONObject.Create();
oErr.AddPair('code' , 'NOT_FOUND');
oErr.AddPair('message', 'Auftrag nicht gefunden');
oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
oRes.AddPair('_OBS_HTTP_STATUS', 404);
oRes.AddPair('error', oErr);
result := oRes.ToJSON();
exit;
end;
// 2) Dateityp gegen eine Whitelist pruefen - niemals gegen eine
// Blacklist, und niemals dem Content-Type allein vertrauen.
cEndung := Lower(ExtractFileExt(cOrigName));
if ((cEndung <> '.jpg') and (cEndung <> '.jpeg') and (cEndung <> '.png') and (cEndung <> '.pdf')) then begin
oErr := TJSONObject.Create();
oErr.AddPair('code' , 'VALIDATION_FAILED');
oErr.AddPair('message', 'Nur JPG, PNG und PDF sind zulässig');
oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
oRes.AddPair('_OBS_HTTP_STATUS', 422);
oRes.AddPair('error', oErr);
result := oRes.ToJSON();
exit;
end;
// 3) Zielname selbst bilden. Den Originalnamen NICHT als Pfad
// verwenden - er kommt vom Client und kann '..\' enthalten.
cZielDir := ZIEL_BASIS + DToS(Date()) + '\' + cUid + '\';
cZielName := DToSF(Now()) + '_' + GlobalUID() + cEndung;
cZielPfad := cZielDir + cZielName;
MyForceDirectories(cZielDir);
if (not FMove(cTmpPfad, cZielPfad)) then begin
// Fehlgeschlagenes Verschieben ist ein Serverproblem -> 500.
// Ein belegter Idempotency-Key bleibt dabei bewusst belegt.
oErr := TJSONObject.Create();
oErr.AddPair('code' , 'INTERNAL_ERROR');
oErr.AddPair('message', 'Datei konnte nicht abgelegt werden');
oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
oRes.AddPair('_OBS_HTTP_STATUS', 500);
oRes.AddPair('error', oErr);
result := oRes.ToJSON();
exit;
end;
// 4) Verweis in der Datenbank ablegen
qNeu := qSqlInit('Y00B6UPL01', oDB, 'AUFTRAG_DOKUMENT');
qNeu.qSet('ad_auftrag' , cUid);
qNeu.qSet('ad_pfad' , cZielPfad);
qNeu.qSet('ad_dateiname', cOrigName);
qNeu.qSet('ad_typ' , cTyp);
qNeu.qSet('ad_kategorie', cKategorie);
qNeu.qSet('ad_datum' , Now());
qNeu.SaveData(NEW_RECORD);
qSqlFree(qNeu);
// 5) 201 mit Location auf die abgelegte Datei
oHdr := TJSONObject.Create();
oHdr.AddPair('Location', '/orders/' + cUid + '/photos/' + cZielName);
oRes.AddPair('_OBS_HTTP_STATUS', 201);
oRes.AddPair('_OBS_HEADERS', oHdr);
oRes.AddPair('dateiname', cZielName);
oRes.AddPair('groesse' , FSize(cZielPfad));
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
wandern, ersetzt der DMS-Aufruf die Schritte 3 bis 5. Welche Funktion dafür zuständig ist, hängt vom eingesetzten Dokumenttyp ab - bitte mit dem OBS-Support
klären.Test mit curl
Einfacher Upload einer Datei (-F erzeugt multipart/form-data):
curl -i -X POST -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
-F "datei=@C:\Fotos\geraet.jpg" ^
"https://api.meinserver.de/orders/4711/photos?kategorie=VORHER"
Antwort:
HTTP/1.1 201 Created Location: /orders/4711/photos/20260817091233_A1B2C3.jpg X-Trace-Id: 20260817T091233123-00001A
{"dateiname":"20260817091233_A1B2C3.jpg","groesse":248113}
Ein Upload an einen Endpunkt ohne Upload-Freigabe:
HTTP/1.1 415 Unsupported Media Type
Eine Datei über der eingestellten Grenze:
HTTP/1.1 413 Payload Too Large
Grosse Dateien: fortsetzbarer Upload
Bei einem Foto aus dem Mobilfunknetz reisst die Verbindung schnell einmal ab. Für solche Fälle überträgt der Client die Datei in Teilstücken und kann nach einem Abbruch dort weitermachen, wo er aufgehört hat. Am Endpunkt-Skript ändert sich dafür nichts - der Server sammelt die Teilstücke selbst ein und ruft das Skript erst auf, wenn die Datei vollständig ist.
Der erste Teil trägt den Dateinamen (Base64-kodiert im Header
Upload-Metadata) und liefert eine Upload-Id zurück:
curl -i -X POST -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
-H "Content-Range: bytes 0-1048575/5000000" ^
-H "Upload-Metadata: filename Z2VyYWV0LmpwZw==,filetype aW1hZ2UvanBlZw==" ^
--data-binary "@teil1.bin" ^
https://api.meinserver.de/orders/4711/photos
HTTP/1.1 308 Resume Incomplete Upload-Id: 7f2a9c84... Upload-Offset: 1048576 Range: bytes=0-1048575
Jeder weitere Teil sendet die Upload-Id mit. Der letzte Teil bekommt die normale Antwort des Skripts (hier 201). Nach einem Abbruch fragt der Client den Stand ab und setzt dort auf:
curl -i -X POST -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
-H "Content-Range: bytes */5000000" -H "Upload-Id: 7f2a9c84..." ^
https://api.meinserver.de/orders/4711/photos
HTTP/1.1 308 Resume Incomplete Upload-Offset: 3145728
Doppelte Uploads vermeiden
Bricht die Verbindung ab, nachdem der Server die Datei schon abgelegt hat, würde ein
Wiederholversuch dasselbe Foto ein zweites Mal einliefern. Dagegen sendet der Client
den Header Idempotency-Key - für jedes Foto einen eigenen, über alle
Wiederholversuche hinweg denselben:
-H "Idempotency-Key: foto-4711-0003"
Der Server erkennt die Wiederholung und liefert die gespeicherte 201-Antwort
samt Header Idempotent-Replay: true zurück, ohne das Skript erneut zu
starten. Es entsteht also weder eine zweite Datei noch ein zweiter DB-Eintrag.
Details: Endpunkte, Abschnitt
Idempotenz.
Was zeigt das Beispiel?
- Endpunkt mit Datei-Upload-Freigabe (
re_upload) und eigener Grössenbegrenzung. - Zugriff auf die hochgeladene Datei über _OBS_UPLOAD_PATH, _OBS_UPLOAD_NAME und _OBS_UPLOAD_CONTENTTYPE.
- Zusatzangaben als Query-Parameter, weil oBody bei Uploads nil ist.
- Whitelist für zulässige Dateitypen und ein selbst gebildeter Zielname - der Originalname landet nie im Pfad.
- Übernahme der temporären Datei mit FMove; ohne diesen Schritt bleibt sie liegen.
- Antwort mit 201 und Location-Header.
- Fortsetzbarer Upload ohne jede Änderung am Skript.
- Schutz gegen doppelte Einlieferung über den Idempotency-Key.
Siehe auch
- Scripting - Upload-Parameter und Protokoll im Detail
- Endpunkte - Freigabe, Grössenlimit, Idempotenz
- Beispiel 5 - Schreibzugriff mit Statuscodes und ETag