OBS/Kostenpflichtige Module/RESTServer/Datei-Download: Unterschied zwischen den Versionen
Böhrer (Diskussion | Beiträge) Die Seite wurde neu angelegt: „<br>Looking for online reputаtion management ѕervices? I do this job. for thiѕ work abslutly secrect work I don't sᥱt it up on my webѕite So please place…“ |
Die Seite wurde neu angelegt: „{{Kostenpflichtige Module}} = Datei-Download = Ein Endpunkt kann statt eines JSON-Körpers eine '''Datei''' ausliefern - PDF, CSV, Bilder, eine APK. Das Skript entscheidet das mit einem einzigen Aufruf, alles Weitere macht der Server: Content-Type, Dateiname, Bereichsanfragen und, bei temporären Dateien, das Aufräumen. Anders als beim Datei-Upload braucht es dafür '''keine Freischaltung''' am E…“ |
||
| Zeile 1: | Zeile 1: | ||
< | {{Kostenpflichtige Module}} | ||
= Datei-Download = | |||
Ein Endpunkt kann statt eines JSON-Körpers eine '''Datei''' ausliefern - PDF, CSV, | |||
Bilder, eine APK. Das Skript entscheidet das mit einem einzigen Aufruf, alles | |||
Weitere macht der Server: Content-Type, Dateiname, Bereichsanfragen und, bei | |||
temporären Dateien, das Aufräumen. | |||
Anders als beim [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]] | |||
braucht es dafür '''keine Freischaltung''' am Endpunkt. Ein Download entsteht | |||
dadurch, dass das Skript ihn erzeugt, nicht dadurch, dass ein Client ihn anfragt. | |||
== Die zwei Aufrufe == | |||
{| class="wikitable" | |||
! Aufruf !! Für !! Nach dem Senden !! Wiederaufsetzbar | |||
|- | |||
| <code>oWriter.SendFile(cPfad, cName, cTyp)</code> || Dateien, die es schon gibt und weiter geben wird || bleibt liegen || '''ja''' (206/Range) | |||
|- | |||
| <code>oWriter.SendTempFile(cPfad, cName, cTyp)</code> || Ergebnisse, die je Abruf entstehen || wird '''gelöscht''' || nein | |||
|} | |||
<syntaxhighlight lang="pascal" line> | |||
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter); | |||
var cUid : string; | |||
cPfad: string; | |||
cName: string; | |||
begin | |||
cUid := oReader.Path('uid'); | |||
if (not BelegGehoertZuMandant(oReader.Claim('tenant'), cUid)) then begin | |||
oWriter.Error(404, 'NOT_FOUND', 'Beleg nicht gefunden'); | |||
exit; | |||
end; | |||
cPfad := BelegPfad(cUid); | |||
cName := 'Rechnung_' + cUid + '.pdf'; | |||
oWriter.SendFile(cPfad, cName, 'application/pdf'); | |||
end; | |||
</syntaxhighlight> | |||
{{Hinweis|'''Datei oder Felder - nicht beides.''' Schreibt ein Skript Felder | |||
'''und''' ruft <code>SendFile</code>, ist das ein Strukturfehler wie eine | |||
unbalancierte Verschachtelung: der Server antwortet '''500''' und nennt die Stelle | |||
im Protokoll. <code>Error()</code> nach <code>SendFile</code> verwirft die Datei | |||
und liefert die Fehlerhülle - die Ablehnung gewinnt.}} | |||
== Was der Server dazu tut == | |||
{| class="wikitable" | |||
! Kopfzeile !! Wert | |||
|- | |||
| Content-Type || der übergebene Typ; ohne Angabe <code>application/octet-stream</code> | |||
|- | |||
| Content-Disposition || <code>attachment</code> mit dem übergebenen Namen, nach RFC 5987 kodiert | |||
|- | |||
| Content-Length || Länge der Datei bzw. des angeforderten Bereichs | |||
|- | |||
| Accept-Ranges || <code>bytes</code> bei <code>SendFile</code>, <code>none</code> bei <code>SendTempFile</code> | |||
|- | |||
| ETag, Last-Modified || nur bei <code>SendFile</code>, aus Grösse und Änderungszeitpunkt | |||
|} | |||
'''Umlaute im Dateinamen''' sind unbedenklich. Header-Werte müssen ASCII sein, | |||
deshalb schreibt der Server zwei Angaben: <code>filename=</code> mit einem | |||
ausgeschriebenen Rückfallnamen (''Rechnung_Müller.pdf'' wird zu | |||
''Rechnung_Mueller.pdf'') und <code>filename*=</code> mit dem echten Namen | |||
UTF-8-kodiert. Jeder heutige Browser nimmt den zweiten. | |||
Der Pfad wird '''nicht geprüft'''. Ein Skript kann über <code>Base.GFile</code> | |||
ohnehin jede Datei des Servers lesen; eine Schranke nur an dieser Stelle würde | |||
Sicherheit vortäuschen, die es nicht gibt. | |||
{{Hinweis|'''Den Pfad niemals ungeprüft aus einem Client-Wert bilden.''' Ein | |||
Endpunkt <code>/public/{name}</code>, der <code>{name}</code> an ein | |||
Basisverzeichnis hängt, liefert bei <code>..%2F..%2Fobs.ini</code> die | |||
Konfiguration aus. Entweder eine feste Zuordnung im Skript (''apk'' -> fester | |||
Pfad), oder eine Tabelle mit freigegebenen Dateien - nie den Rohwert.}} | |||
== Wiederaufnahme abgebrochener Downloads == | |||
Bei <code>SendFile</code> wertet der Server den Kopf <code>Range</code> aus. | |||
Unterstützt wird '''ein''' Bereich je Anfrage, in den drei Formen, die Clients | |||
tatsächlich senden: | |||
{| class="wikitable" | |||
! Range !! Bedeutung | |||
|- | |||
| <code>bytes=0-1023</code> || fester Bereich | |||
|- | |||
| <code>bytes=500-</code> || ab Offset bis zum Ende | |||
|- | |||
| <code>bytes=-500</code> || die letzten 500 Bytes | |||
|} | |||
Der Server antwortet dann mit '''206 Partial Content''' und | |||
<code>Content-Range: bytes <von>-<bis>/<gesamt></code>. | |||
{| class="wikitable" | |||
! Fall !! Antwort | |||
|- | |||
| Kein <code>Range</code>-Kopf || '''200''' mit der ganzen Datei | |||
|- | |||
| Mehrere Bereiche (Komma) || '''200''' mit der ganzen Datei - die Spezifikation erlaubt das ausdrücklich | |||
|- | |||
| Bereich hinter dem Dateiende || '''416 Range Not Satisfiable''' mit <code>Content-Range: bytes */<gesamt></code> | |||
|- | |||
| <code>If-Range</code> stimmt nicht mehr || '''200''' mit der ganzen Datei | |||
|} | |||
'''Warum <code>If-Range</code> zählt:''' Setzt ein Client einen Download fort und | |||
die Datei hat sich zwischenzeitlich geändert, entstünde aus zwei Ständen eine | |||
Datei, die erst beim Öffnen auffällt. Schickt der Client seinen ETag in | |||
<code>If-Range</code> mit und passt der nicht mehr, liefert der Server lieber alles | |||
neu. | |||
{{Hinweis|'''SendTempFile kann nicht fortgesetzt werden''', und das ist keine | |||
Lücke: Die Datei ist nach dem Senden gelöscht, ein zweiter Abruf fände sie nicht | |||
mehr vor - und eine Neuerzeugung lieferte andere Bytes, sobald sich die Daten | |||
geändert haben. Der Server meldet deshalb <code>Accept-Ranges: none</code>, damit | |||
ein Client es gar nicht erst versucht.}} | |||
== Öffentliche Dateien ohne API-Key == | |||
Für Artefakte, die für jeden bestimmt sind - eine Test-APK, ein Handbuch - kann | |||
der Endpunkt ohne <code>apikey</code>-Header erreichbar sein. Dafür genügt | |||
Konfiguration, siehe | |||
[[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]]: | |||
# Zugang mit <code>ra_apikey = *</code>, <code>ra_jwt = 0</code>, <code>ra_cors_origin</code> '''leer''' | |||
# In <code>RESTSRV_ACCESS</code> '''nur''' diesen einen Endpunkt freigeben | |||
Die zweite Zeile ist die eigentliche Grenze: Für jeden anderen Endpunkt antwortet | |||
der Server '''403''', auch wenn der Zugang existiert. | |||
{{Hinweis|'''Nur für wirklich öffentliche Dateien.''' Ohne API-Key kennt der | |||
Endpunkt weder Mandant noch Rolle - es gibt nichts, woran er eine Berechtigung | |||
festmachen könnte ausser dem, was in der URL steht. Kundenbelege gehören deshalb | |||
über den authentifizierten Weg, nicht hierher.}} | |||
Für eine Android-APK kommt hinzu: Content-Type | |||
<code>application/vnd.android.package-archive</code>, am Gerät muss "Unbekannte | |||
Apps installieren" für den Browser freigegeben sein, und die Signatur sollte über | |||
alle Testversionen konstant bleiben - sonst gibt es beim späteren Wechsel in den | |||
Play Store auf jedem Testgerät einen Installationskonflikt. | |||
== Grenzen == | |||
{| class="wikitable" | |||
! Was !! Stand | |||
|- | |||
| HEAD || wird '''nicht''' unterstützt (''405''). Browser brauchen es nicht; relevant nur für <code>wget --continue</code>, <code>curl -C -</code> und Download-Manager, die meist auf GET zurückfallen. | |||
|- | |||
| Mehrere Bereiche je Anfrage || nicht unterstützt (ganze Datei mit 200) | |||
|- | |||
| Liegengebliebene Temp-Dateien || Bricht der Server mitten im Senden ab, bleibt eine <code>SendTempFile</code>-Datei stehen. Das temporäre Verzeichnis wird von aussen geleert. | |||
|- | |||
| Idempotenz || Eine Dateiantwort wird '''nicht''' im Idempotenz-Store festgeschrieben - sie liesse sich nicht wiedergeben. Der <code>Idempotency-Key</code> bleibt danach benutzbar. | |||
|} | |||
Aktuelle Version vom 14. September 2026, 09:07 Uhr
- 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
Datei-Download
Ein Endpunkt kann statt eines JSON-Körpers eine Datei ausliefern - PDF, CSV, Bilder, eine APK. Das Skript entscheidet das mit einem einzigen Aufruf, alles Weitere macht der Server: Content-Type, Dateiname, Bereichsanfragen und, bei temporären Dateien, das Aufräumen.
Anders als beim Datei-Upload braucht es dafür keine Freischaltung am Endpunkt. Ein Download entsteht dadurch, dass das Skript ihn erzeugt, nicht dadurch, dass ein Client ihn anfragt.
Die zwei Aufrufe
| Aufruf | Für | Nach dem Senden | Wiederaufsetzbar |
|---|---|---|---|
oWriter.SendFile(cPfad, cName, cTyp) |
Dateien, die es schon gibt und weiter geben wird | bleibt liegen | ja (206/Range) |
oWriter.SendTempFile(cPfad, cName, cTyp) |
Ergebnisse, die je Abruf entstehen | wird gelöscht | nein |
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);
var cUid : string;
cPfad: string;
cName: string;
begin
cUid := oReader.Path('uid');
if (not BelegGehoertZuMandant(oReader.Claim('tenant'), cUid)) then begin
oWriter.Error(404, 'NOT_FOUND', 'Beleg nicht gefunden');
exit;
end;
cPfad := BelegPfad(cUid);
cName := 'Rechnung_' + cUid + '.pdf';
oWriter.SendFile(cPfad, cName, 'application/pdf');
end;und ruft SendFile, ist das ein Strukturfehler wie eine
unbalancierte Verschachtelung: der Server antwortet 500 und nennt die Stelle
im Protokoll. Error() nach SendFile verwirft die Datei
Was der Server dazu tut
| Kopfzeile | Wert |
|---|---|
| Content-Type | der übergebene Typ; ohne Angabe application/octet-stream
|
| Content-Disposition | attachment mit dem übergebenen Namen, nach RFC 5987 kodiert
|
| Content-Length | Länge der Datei bzw. des angeforderten Bereichs |
| Accept-Ranges | bytes bei SendFile, none bei SendTempFile
|
| ETag, Last-Modified | nur bei SendFile, aus Grösse und Änderungszeitpunkt
|
Umlaute im Dateinamen sind unbedenklich. Header-Werte müssen ASCII sein,
deshalb schreibt der Server zwei Angaben: filename= mit einem
ausgeschriebenen Rückfallnamen (Rechnung_Müller.pdf wird zu
Rechnung_Mueller.pdf) und filename*= mit dem echten Namen
UTF-8-kodiert. Jeder heutige Browser nimmt den zweiten.
Der Pfad wird nicht geprüft. Ein Skript kann über Base.GFile
ohnehin jede Datei des Servers lesen; eine Schranke nur an dieser Stelle würde
Sicherheit vortäuschen, die es nicht gibt.
Endpunkt /public/{name}, der {name} an ein
Basisverzeichnis hängt, liefert bei ..%2F..%2Fobs.ini die
Konfiguration aus. Entweder eine feste Zuordnung im Skript (apk -> fester
Wiederaufnahme abgebrochener Downloads
Bei SendFile wertet der Server den Kopf Range aus.
Unterstützt wird ein Bereich je Anfrage, in den drei Formen, die Clients
tatsächlich senden:
| Range | Bedeutung |
|---|---|
bytes=0-1023 |
fester Bereich |
bytes=500- |
ab Offset bis zum Ende |
bytes=-500 |
die letzten 500 Bytes |
Der Server antwortet dann mit 206 Partial Content und
Content-Range: bytes <von>-<bis>/<gesamt>.
| Fall | Antwort |
|---|---|
Kein Range-Kopf |
200 mit der ganzen Datei |
| Mehrere Bereiche (Komma) | 200 mit der ganzen Datei - die Spezifikation erlaubt das ausdrücklich |
| Bereich hinter dem Dateiende | 416 Range Not Satisfiable mit Content-Range: bytes */<gesamt>
|
If-Range stimmt nicht mehr |
200 mit der ganzen Datei |
Warum If-Range zählt: Setzt ein Client einen Download fort und
die Datei hat sich zwischenzeitlich geändert, entstünde aus zwei Ständen eine
Datei, die erst beim Öffnen auffällt. Schickt der Client seinen ETag in
If-Range mit und passt der nicht mehr, liefert der Server lieber alles
neu.
Lücke: Die Datei ist nach dem Senden gelöscht, ein zweiter Abruf fände sie nicht
mehr vor - und eine Neuerzeugung lieferte andere Bytes, sobald sich die Daten
geändert haben. Der Server meldet deshalb Accept-Ranges: none, damit
Öffentliche Dateien ohne API-Key
Für Artefakte, die für jeden bestimmt sind - eine Test-APK, ein Handbuch - kann
der Endpunkt ohne apikey-Header erreichbar sein. Dafür genügt
Konfiguration, siehe
Zugänge:
- Zugang mit
ra_apikey = *,ra_jwt = 0,ra_cors_originleer - In
RESTSRV_ACCESSnur diesen einen Endpunkt freigeben
Die zweite Zeile ist die eigentliche Grenze: Für jeden anderen Endpunkt antwortet der Server 403, auch wenn der Zugang existiert.
Endpunkt weder Mandant noch Rolle - es gibt nichts, woran er eine Berechtigung festmachen könnte ausser dem, was in der URL steht. Kundenbelege gehören deshalb
über den authentifizierten Weg, nicht hierher.Für eine Android-APK kommt hinzu: Content-Type
application/vnd.android.package-archive, am Gerät muss "Unbekannte
Apps installieren" für den Browser freigegeben sein, und die Signatur sollte über
alle Testversionen konstant bleiben - sonst gibt es beim späteren Wechsel in den
Play Store auf jedem Testgerät einen Installationskonflikt.
Grenzen
| Was | Stand |
|---|---|
| HEAD | wird nicht unterstützt (405). Browser brauchen es nicht; relevant nur für wget --continue, curl -C - und Download-Manager, die meist auf GET zurückfallen.
|
| Mehrere Bereiche je Anfrage | nicht unterstützt (ganze Datei mit 200) |
| Liegengebliebene Temp-Dateien | Bricht der Server mitten im Senden ab, bleibt eine SendTempFile-Datei stehen. Das temporäre Verzeichnis wird von aussen geleert.
|
| Idempotenz | Eine Dateiantwort wird nicht im Idempotenz-Store festgeschrieben - sie liesse sich nicht wiedergeben. Der Idempotency-Key bleibt danach benutzbar.
|