OBS/Kostenpflichtige Module/RESTServer/Beispiel6

Aus OBS Wiki
Version vom 17. August 2026, 07:54 Uhr von Rademacker (Diskussion | Beiträge) (Die Seite wurde neu angelegt: „{{Kostenpflichtige Module}} =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 einf…“)
Zur Navigation springen Zur Suche springen
Kostenpflichtige Module

Internet-Shop
UPS
IMS Professional
SMS
Mehrlager-Verwaltung
Mehrsprachen Modul
Multilanguage Modul
EVA Marketing Tool
Termin-Projekte
Edifact-Schnittstelle
Backup Überwachung Email
OBS Geo Daten
DeliSprint / DPD
Filialen
Cashback
Moebelschnittstelle
Dokumenten Manager
DocuWare-Schnittstelle
OFML-Kalkulation
Versicherungsschaden
Gutschriftsanzeigen
Kameraverwaltung
DataInOut
OpenMasterData / IDS
Sammelpositionen



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
  • Berechtigung: Zugang Mobile-App für den Endpunkt freischalten
HINWEIS: Ohne den Haken Datei-Upload lehnt der Server jeden Upload mit

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:

  • oBody ist bei Uploads immer nil. 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;
HINWEIS: Soll die Datei statt in ein Verzeichnis in das OBS-Dokumenten-System

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
HINWEIS: Der Zwischenstand liegt im Arbeitsspeicher des Dienstes. Wird der REST-Dienst neu gestartet, muss ein unvollständiger Upload von vorn beginnen.

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