POST /api/fehlende stellt dieselbe Frage wie das Dashboard (welche Namen
haben kein Bild?), mit Token statt Sitzung. Das Fotostudio soll "fehlt" so
beantworten, wie der Laden es sieht -- fuer die Kleidung sind das rund 8.000
Namen, mit /exists ebenso viele Anfragen, hier zwei.
Die Rechnung steht jetzt EINMAL in server/src/fehlende.ts; die Dashboard-Route
ruft sie auf. Die Antwortform bleibt (da als Zahl), dazu kommt `schreibweise`.
DABEI GEFUNDEN: die alte Rechnung schrieb jeden Namen klein, "weil so
abgelegt wird". Seit a373b96 (22.08.) stimmt das nicht mehr. Gemessen am
03.10. ueber /f/: items/WEAPON_PISTOL.png 200, items/weapon_pistol.png 404 --
die Abfrage meldete also jede Waffe als fehlend, die da lag. Jetzt wird genau
verglichen; liegt etwas nur unter anderer Schreibweise, steht es zusaetzlich
in `schreibweise` (umbenennen statt neu aufnehmen). docs/API.md sagte
dasselbe Falsche und riet zu item:lower() -- berichtigt.
Geprueft in einem eigenen Container gegen eine Kopie der Datenbank (per
SQLite-Sicherung gezogen, danach geloescht), mit Wegwerf-Token: 13/13 --
401 ohne Token, 400 mit Text bei kaputtem JSON, fehlendem Platzhalter, ohne
Namen und bei 5001 Namen; WEAPON_PISTOL da, weapon_pistol fehlt und steht in
schreibweise, ein erfundener Name fehlt ohne schreibweise. Ernstfall: 7.928
lokale Kleidungsbilder des Fotostudios, 2.591 davon liegen im Dienst (33 %,
eine Stichprobe ueber /f/ hatte 11 von 40 ergeben).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
571 lines
22 KiB
Markdown
571 lines
22 KiB
Markdown
# Die API von d4rk_media
|
||
|
||
Diese Seite ist für den Fall geschrieben, dass jemand ein Skript anbindet und
|
||
etwas nicht tut, was es soll. Sie nennt deshalb nicht nur die Endpunkte,
|
||
sondern auch **was schiefgehen kann und wie die Antwort dann aussieht**.
|
||
|
||
Der Dienst läuft unter `https://media.d4rkst3r.de`. Dateien liegen darunter
|
||
unter `/f/`.
|
||
|
||
---
|
||
|
||
## Der Grundsatz: der Pfad ist der Schlüssel
|
||
|
||
`vehicles/adder.webp` bleibt `vehicles/adder.webp`. Die öffentliche Adresse
|
||
entsteht daraus, indem der Ursprung davorgesetzt wird:
|
||
|
||
```
|
||
vehicles/adder.webp → https://media.d4rkst3r.de/f/vehicles/adder.webp
|
||
```
|
||
|
||
Deshalb kann die Vorlage in einem Handy-Skript schlicht `{model}.webp` lauten.
|
||
**Die Adressen stehen nirgends in der Datenbank** — sie werden bei jeder
|
||
Antwort neu gebaut. Ein Umzug auf einen anderen Namen ist eine geänderte
|
||
Variable, kein Datenbankeingriff.
|
||
|
||
### Erlaubte Pfade
|
||
|
||
Jeder Teil zwischen den Schrägstrichen muss mit einem **Buchstaben oder einer
|
||
Ziffer** beginnen; danach sind zusätzlich Punkt, Strich und Unterstrich
|
||
erlaubt. Höchstens 8 Ebenen, höchstens 200 Zeichen.
|
||
|
||
Damit sind `..`, versteckte Dateien und alles mit Backslash von vornherein
|
||
draußen. Was nicht passt, wird **abgelehnt und nicht zurechtgebogen** — sonst
|
||
läge eine Datei unter einem anderen Namen als dem, den der Aufrufer erwartet,
|
||
und niemand merkte es, bis das Bild fehlt.
|
||
|
||
> Ein führender Unterstrich fällt durch. Das ist kein erfundener Fall: das
|
||
> Fotostudio legt seine Leerbilder als `_plate` ab.
|
||
|
||
### Groß- und Kleinschreibung
|
||
|
||
**Die Schreibweise bleibt, und sie zählt.** `WEAPON_SMG.png` liegt als
|
||
`WEAPON_SMG.png`, und `/f/items/weapon_smg.png` ist dann ein 404.
|
||
|
||
> **Berichtigt am 03.10.2026.** Hier stand bis dahin, alles komme **klein**
|
||
> an und eine Vorlage solle `item:lower()` rufen. Das gilt seit `a373b96`
|
||
> (22.08.2026) nicht mehr: ox_inventory fragt unter dem exakten Item-Namen,
|
||
> und dessen Waffen heißen `WEAPON_*`. Gemessen am 03.10.2026:
|
||
> `items/WEAPON_PISTOL.png` 200, `items/weapon_pistol.png` 404. Wer der alten
|
||
> Zeile folgte, baute Adressen, die ins Leere zeigen — und die
|
||
> Dashboard-Abfrage „Fehlende“ tat genau das und meldete jede Waffe als
|
||
> fehlend.
|
||
|
||
Für ein Skript heißt das: **den Namen so einsetzen, wie er ist.** Klein
|
||
gemacht wird nur die Endung.
|
||
|
||
```lua
|
||
local url = ('https://media.d4rkst3r.de/f/items/%s.png'):format(item)
|
||
```
|
||
|
||
Ersetzt werden beim Hochladen über das Fenster weiterhin Zeichen, die in einer
|
||
Adresse nichts verloren haben (Leerzeichen, Klammern, Umlaute). Über
|
||
`/api/upload` mit eigenem `X-Path` steht der Pfad so, wie er geschickt wird.
|
||
|
||
---
|
||
|
||
## Dieselbe Adresse, zwei Fassungen
|
||
|
||
Neben jedem PNG und JPEG kann eine **WebP-Fassung** liegen. Ausgeliefert wird
|
||
sie unter **derselben Adresse** — welche der beiden kommt, entscheidet die
|
||
Kopfzeile `Accept` des Aufrufers:
|
||
|
||
```
|
||
Accept: …image/webp… → Content-Type: image/webp (klein)
|
||
kein Accept → Content-Type: image/png (das Original)
|
||
```
|
||
|
||
Nachgemessen an `probe/probe-webp.png`: 6448 Bytes für `curl`, 2758 Bytes für
|
||
einen Browser. **Für ein Skript ändert sich nichts** — Lua schickt kein
|
||
`Accept: image/webp` und bekommt weiterhin Byte für Byte das Original.
|
||
|
||
Zwei Dinge, die daran hängen:
|
||
|
||
- Die Antwort trägt `Vary: Accept`. Ohne die Zeile legte ein Zwischenspeicher
|
||
die WebP-Fassung für alle ab, auch für die, die sie nicht lesen können.
|
||
- Die beiden Fassungen haben **verschiedene ETags** (die WebP-Fassung endet auf
|
||
`-w`). Sonst bekäme jemand auf seinen `If-None-Match` hin ein 304 für das
|
||
falsche Bild.
|
||
|
||
Erzeugt werden die Fassungen beim Hochladen; für alles, was vorher schon da
|
||
war, gibt es im Dashboard unter **Speicher → Wartung** den Knopf
|
||
„Sparsame Fassungen erzeugen". Wo WebP nicht kleiner ausfällt, entsteht keine —
|
||
dann bleibt es beim Original.
|
||
|
||
---
|
||
|
||
## Umziehen von Fivemanage — ohne Lua anzufassen
|
||
|
||
Auf einem laufenden Server stecken die Fivemanage-Aufrufe in einem Dutzend
|
||
Ressourcen: Kamera, Handy, MDT, Fahrzeugstudio. Sie alle umzuschreiben heißt,
|
||
fremde Skripte anzufassen — und beim nächsten Update wieder.
|
||
|
||
**Der Dienst spricht deshalb ihre Sprache.** Der Umzug ist eine Zeile je Skript:
|
||
`api.fivemanage.com` → `media.d4rkst3r.de`. Der Schlüssel bleibt derselbe
|
||
Kopf, die Antwort dieselbe Form. Als Schlüssel setzt du einen **Token dieses
|
||
Dienstes** ein.
|
||
|
||
| Fivemanage | hier | Feld | Antwort |
|
||
|---|---|---|---|
|
||
| `POST /api/image` | ✅ gleich | `image` (oder `file`) | `{ "url": …, "id": …, "path": … }` |
|
||
| `POST /api/video` | ✅ gleich | `video` | dito |
|
||
| `POST /api/audio` | ✅ gleich | `audio` | dito |
|
||
| `POST /api/v3/file` | ✅ gleich | `file`, `path?`, `filename?` | `{ "status": "ok", "data": { "id", "url" } }` |
|
||
|
||
Der Schlüssel steht **nackt** im `Authorization`-Kopf, ohne `Bearer` — genau so
|
||
wie bei Fivemanage. `Bearer …` geht weiterhin auch.
|
||
|
||
```lua
|
||
-- vorher
|
||
exports['screenshot-basic']:requestScreenshotUpload(
|
||
'https://api.fivemanage.com/api/image', 'image',
|
||
{ headers = { Authorization = apiKey } }, cb)
|
||
|
||
-- nachher — eine Zeile
|
||
exports['screenshot-basic']:requestScreenshotUpload(
|
||
'https://media.d4rkst3r.de/api/image', 'image',
|
||
{ headers = { Authorization = apiKey } }, cb)
|
||
```
|
||
|
||
**Wo die Dateien landen:** unter dem Präfix des Tokens (sonst `uploads/`), mit
|
||
dem Anfang des SHA-256 im Namen — `screenshots/shot-841814b4089b.png`. Das ist
|
||
Absicht: Bildschirmfotos heißen alle `screenshot.png`, und ohne eigenen Namen
|
||
je Aufruf überschriebe das zweite das erste. Gleicher Inhalt ergibt denselben
|
||
Namen, verschiedener Inhalt einen anderen — dieselbe Eigenschaft, die
|
||
Fivemanages ID auch hat.
|
||
|
||
**Was NICHT übernommen wird:** `metadata` wird angenommen und ignoriert (es gibt
|
||
hier keine Spielerdaten an einer Datei), `retentionExempt` ebenso — dieser
|
||
Dienst löscht nichts von selbst. Presigned URLs gibt es nicht: der Upload geht
|
||
direkt hierher.
|
||
|
||
> Beide Formen sind **aus echtem Code abgelesen**, nicht geraten:
|
||
> [fivemanage/sdk](https://github.com/fivemanage/sdk) für v3 und
|
||
> [Awleks/Devm-Camera](https://github.com/Awleks/Devm-Camera) für den älteren
|
||
> Weg über `screenshot-basic`.
|
||
|
||
---
|
||
|
||
## Für eine Statusseite
|
||
|
||
Zwei Adressen, beide öffentlich, beide ohne Anmeldung — und sie beantworten
|
||
**verschiedene** Fragen.
|
||
|
||
```
|
||
GET /health -> 200 {"ok":true}
|
||
GET /status -> 200 | 503 mit Begründung
|
||
```
|
||
|
||
`/health` beweist genau eines: **der Prozess nimmt Anfragen an.** Daran hängt
|
||
auch der `HEALTHCHECK` des Containers, und dort ist eine billige Prüfung
|
||
richtig — eine schwere, die bei einer langsamen Platte einmal ausfällt, ließe
|
||
Docker den Container neu starten, also genau dann, wenn er unter Last steht.
|
||
|
||
`/status` sieht wirklich nach:
|
||
|
||
| Prüfung | wie |
|
||
|---|---|
|
||
| `datenbank` | eine echte Abfrage gegen eine echte Tabelle |
|
||
| `platte` | schreiben **und wieder löschen** — der einzige Beweis |
|
||
| `ausliefern` | eine zufällige Datei aus der Datenbank auf der Platte nachmessen |
|
||
| `bestand` | kein Eintrag, aber Dateien da? Dann stimmt etwas nicht |
|
||
| `platz` | ab 95 % belegt |
|
||
| `sicherung` | älter als 26 Stunden oder fehlgeschlagen |
|
||
|
||
```json
|
||
{ "dienst": "d4rk_media", "ok": true, "stand": "gesund", "seit": 4271,
|
||
"pruefungen": [ { "was": "datenbank", "ok": true }, … ] }
|
||
```
|
||
|
||
**Zwei Stufen, und der Unterschied ist der zwischen „tut es nicht" und „braucht
|
||
Aufmerksamkeit".** Datenbank, Platte, Ausliefern und Bestand sind das Geschäft
|
||
dieses Dienstes — fällt eines aus, ist er `gestoert` und die Antwort ist
|
||
**503**. Eine alte Sicherung oder eine volle Platte machen ihn
|
||
`beeintraechtigt`: er liefert weiter aus, die Antwort bleibt **200**.
|
||
|
||
Wer auch das rot haben will, hängt `?streng=1` an — dann gibt alles außer
|
||
`gesund` eine 503.
|
||
|
||
Die Prüfung `sicherung` sagt zusätzlich, an **wie vielen Orten** das letzte
|
||
Archiv liegt (`an 3 Orten`, oder `nur an einem Ort`). Eine Sicherung, die es
|
||
nur einmal gibt, ist grün und trotzdem eine, die ein Plattenausfall mitnimmt.
|
||
|
||
### In Uptime Kuma
|
||
|
||
```
|
||
Dienst läuft https://media.d4rkst3r.de/status
|
||
Braucht Pflege https://media.d4rkst3r.de/status?streng=1
|
||
```
|
||
|
||
> **Warum `bestand` dabei ist.** Nachgemessen an genau diesem Dienst: wird eine
|
||
> Sicherung zurückgespielt, ohne `media.db-wal` daneben zu entfernen, spielt
|
||
> SQLite das WAL der **alten** Datenbank über die neue. Ergebnis: 0 Einträge,
|
||
> während 4452 Dateien danebenliegen. Der Dienst kommt hoch, `/health` ist
|
||
> grün, die Mediathek ist leer.
|
||
>
|
||
> ```
|
||
> /health sagt: HTTP 200
|
||
> /status sagt: HTTP 503 kein Eintrag in der Datenbank, aber Dateien auf der Platte
|
||
> ```
|
||
|
||
Was **nicht** hinausgeht: Dateizahlen, Größen, Pfade, Tokennamen, Benutzer. Die
|
||
einzige Zahl ist die Laufzeit in Sekunden.
|
||
|
||
---
|
||
|
||
## Anmeldung: Token
|
||
|
||
Alle Skript-Wege brauchen einen Token im Kopf:
|
||
|
||
```
|
||
Authorization: Bearer d4rk_…
|
||
```
|
||
|
||
Tokens werden im Dashboard angelegt. Der Klartext wird **genau einmal** gezeigt
|
||
— danach steht nur noch sein Hash in der Datenbank, und auch der Dienst kann
|
||
ihn nicht mehr herausgeben.
|
||
|
||
Ein Token kann **sechs** Fesseln haben — alle im Dashboard einstellbar, auch
|
||
nachträglich (`PATCH`, ohne dass sich der Schlüssel ändert):
|
||
|
||
| Fessel | Wirkung |
|
||
|---|---|
|
||
| Präfix | darf nur unterhalb dieses Pfades schreiben, z. B. `vehicles/` |
|
||
| darf löschen | ohne dieses Recht antwortet `DELETE` mit 403 |
|
||
| größte Datei | schärfer als `MAX_UPLOAD_MB`; darüber 413 |
|
||
| Kontingent | wieviel dieser Token **insgesamt** halten darf; darüber 413 |
|
||
| erlaubte Arten | Bilder, Video, Ton, Dokumente, Anderes; sonst 403 |
|
||
| gültig bis | danach 401 mit dem Datum im Satz |
|
||
|
||
> **Das Kontingent ist etwas anderes als die Größe einer Datei.** Ein Token mit
|
||
> „höchstens 2 MB je Bild" konnte die Platte trotzdem füllen — es brauchte nur
|
||
> genug Bilder. Gezählt wird über das, was wirklich liegt, nicht über einen
|
||
> mitlaufenden Zähler:
|
||
>
|
||
> ```
|
||
> Kontingent erschoepft: dieser Token haelt 12 KB von 20 KB,
|
||
> und diese Datei braucht 12 KB.
|
||
> ```
|
||
|
||
### ⚠️ Bilder können kleiner zurückkommen, als sie hingingen
|
||
|
||
Ein Token kann eine **größte Kantenlänge** tragen. Ist sie gesetzt und das Bild
|
||
größer, rechnet der Dienst es herunter, **bevor** er es ablegt:
|
||
|
||
```
|
||
3000 × 2000 hingeschickt → 512 × 341 abgelegt, 82 KB → 3 KB
|
||
```
|
||
|
||
Für ein Skript heißt das zweierlei:
|
||
|
||
1. **`size` in der Antwort ist die Größe der abgelegten Datei**, nicht die der
|
||
geschickten.
|
||
2. **`sha256` ebenso.** Wer den Hash vor dem Upload selbst rechnet und mit der
|
||
Antwort vergleicht, bekommt bei gesetzter Kantenlänge einen Unterschied —
|
||
und der ist kein Fehler. Der Hash gehört zu dem, was liegt; alles andere
|
||
wäre gelogen.
|
||
|
||
Kleinere Bilder werden **nicht** aufgeblasen, und Video, Ton und Dokumente
|
||
bleiben unberührt. Ohne gesetzte Kantenlänge passiert gar nichts — der
|
||
Standard ist „ablegen, wie es kommt".
|
||
|
||
---
|
||
|
||
## POST /api/upload
|
||
|
||
Nimmt die Datei in einer von **drei Rumpfformen** entgegen:
|
||
|
||
1. `multipart/form-data` mit dem Feld `file`
|
||
2. Base64-Text, wenn `X-Encoding: base64` gesetzt ist
|
||
3. rohe Bytes, sonst
|
||
|
||
### Kopfzeilen
|
||
|
||
| Kopf | Bedeutung |
|
||
|---|---|
|
||
| `X-Path` | Wohin die Datei kommt. **Ohne ihn** wird aus dem SHA-256 ein Pfad gebaut — dann ist die Adresse nicht mehr vorhersagbar. |
|
||
| `X-Encoding` | `base64`, wenn der Rumpf Base64-Text ist. Ein Data-URL-Vorspann (`data:image/webp;base64,`) wird abgeschnitten. |
|
||
| `X-Overwrite` | `false` verweigert das Überschreiben und antwortet mit 409. Standard ist überschreiben. |
|
||
| `X-Mime` | Überschreibt die Art. Sonst entscheidet die Endung. |
|
||
|
||
### Antwort
|
||
|
||
```json
|
||
{
|
||
"url": "https://media.d4rkst3r.de/f/vehicles/adder.webp",
|
||
"path": "vehicles/adder.webp",
|
||
"size": 132760,
|
||
"sha256": "36cb4ddf…",
|
||
"mime": "image/webp",
|
||
"replaced": false
|
||
}
|
||
```
|
||
|
||
Trägt der Token eine Kantenlänge und wurde wirklich gerechnet, kommt ein Feld
|
||
dazu — sonst steht es gar nicht erst da, denn ein Feld, das immer dasteht, sagt
|
||
nichts:
|
||
|
||
```json
|
||
"verkleinert": { "von": "3000x2000", "auf": "512x341" }
|
||
```
|
||
|
||
### Was schiefgehen kann
|
||
|
||
| Antwort | Grund |
|
||
|---|---|
|
||
| 401 | Token fehlt oder ist unbekannt |
|
||
| 403 | Der Token darf dort nicht schreiben (Präfix-Fessel) |
|
||
| 400 | Pfad unzulässig, Rumpf leer, kaputtes Base64 |
|
||
| 409 | `X-Overwrite: false` und der Pfad ist belegt |
|
||
| 413 | größer als `MAX_UPLOAD_MB` (Standard 64), **oder** größer als die Grenze dieses Tokens, **oder** sein Kontingent ist erschöpft — welches, steht im Satz |
|
||
|
||
Jede Fehlerantwort ist JSON mit einem Feld `error` und einem **Satz auf
|
||
Deutsch**. Ein nackter 500 ohne Text ist genau der Fehler, wegen dem dieser
|
||
Dienst gebaut wurde.
|
||
|
||
---
|
||
|
||
## Aus einer FiveM-Resource
|
||
|
||
```lua
|
||
local TOKEN = 'd4rk_…' -- im Dashboard anlegen
|
||
|
||
local function hochladen(pfad, bytes, cb)
|
||
PerformHttpRequest('https://media.d4rkst3r.de/api/upload',
|
||
function(status, body)
|
||
if status ~= 200 then return cb(nil, body) end
|
||
cb(json.decode(body).url)
|
||
end, 'POST', B64.encode(bytes), {
|
||
['Authorization'] = 'Bearer ' .. TOKEN,
|
||
['X-Path'] = pfad,
|
||
['X-Encoding'] = 'base64',
|
||
['Content-Type'] = 'text/plain; charset=utf-8',
|
||
})
|
||
end
|
||
```
|
||
|
||
**Warum Base64 und nicht die rohen Bytes.** `PerformHttpRequest` nimmt den
|
||
Rumpf als Lua-Zeichenkette, und ob Nullbytes darin unverändert ankommen, hat
|
||
hier niemand nachgemessen — ein PNG besteht zu gutem Teil daraus. Base64 kostet
|
||
ein Drittel mehr auf der Leitung und nimmt die Frage aus dem Weg. Nachgemessen
|
||
ist: Base64 kommt mit demselben SHA-256 an wie die rohen Bytes.
|
||
|
||
**Warum vom Server und nicht aus der NUI.** Die NUI läuft unter `https`, und
|
||
CEF hebt jede `http`-Adresse selbst dorthin an. Ein Upload aus dem Fenster
|
||
heraus wäre entweder Mixed Content oder ginge ins Leere.
|
||
|
||
---
|
||
|
||
## GET /api/exists?pfad=<pfad>
|
||
|
||
Für „nur fehlende" in einem Serienlauf: eine winzige Antwort gegen 130 KB
|
||
Upload.
|
||
|
||
> **Diese Form und nicht mehr `/api/exists/<pfad>`** — der Grund ist
|
||
> nachgemessen und unangenehm. Die alte Form endet auf `.webp`, und Nginx
|
||
> Proxy Managers `assets.conf` greift auf **jede** URL mit Bildendung:
|
||
>
|
||
> ```nginx
|
||
> location ~* ^.*\.(css|js|jpe?g|gif|png|webp|...)$ {
|
||
> proxy_cache public-cache;
|
||
> proxy_ignore_headers Set-Cookie Cache-Control Expires ...;
|
||
> proxy_cache_valid any 30m;
|
||
> }
|
||
> ```
|
||
>
|
||
> Der Proxy legt die berechtigte Antwort weg und liefert sie danach **an jeden
|
||
> aus, auch ohne Token** — gemessen: 200 mit voller Auskunft, `Authorization`
|
||
> gar nicht gesetzt. Unser eigenes `Cache-Control` hilft nicht,
|
||
> `proxy_ignore_headers` wirft es weg. Und es geht auch andersherum: landet
|
||
> zuerst ein `401` im Zwischenspeicher, bekommen ihn 30 Minuten lang alle.
|
||
>
|
||
> Die Abfrageform endet nie auf eine Bildendung und ist deshalb **von sich aus**
|
||
> dicht — ohne von einer Zeile in einer fremden Oberfläche abzuhängen. Die alte
|
||
> Form bleibt, weil Skripte sie benutzen; neu gebaut wird mit dieser.
|
||
|
||
```json
|
||
{ "exists": true, "path": "vehicles/adder.webp",
|
||
"url": "…", "size": 132760, "sha256": "…", "updated_at": 1786466462488 }
|
||
```
|
||
|
||
> **Drei Zustände, nicht zwei.** Wer einen Netzfehler als „nicht da" durchgehen
|
||
> lässt, lädt bei einem Husten alles ein zweites Mal hoch. Ein Aufrufer sollte
|
||
> zwischen `exists: false` und „ich weiß es nicht" unterscheiden.
|
||
|
||
---
|
||
|
||
## POST /api/fehlende
|
||
|
||
Dieselbe Frage wie im Dashboard unter **Fehlende**, aber mit Token: welche
|
||
dieser Namen haben **kein** Bild? Für „nur fehlende“ über einen ganzen
|
||
Katalog — die Kleidung des Fotostudios sind rund 8.000 Namen, das wären
|
||
8.000 Anfragen an `/api/exists` und hier zwei.
|
||
|
||
```json
|
||
{ "muster": "clothing/{model}.png", "namen": ["m_c_11_base_84", "…"] }
|
||
```
|
||
|
||
`namen` darf auch ein Textblock sein (Zeilen, Kommas oder Semikolons).
|
||
Höchstens **5000 Namen** je Anfrage, mehr gibt einen 400 mit Text.
|
||
|
||
```json
|
||
{ "muster": "clothing/{model}.png", "geprueft": 2, "da": 1,
|
||
"fehlt": [{ "name": "m_c_11_base_84", "pfad": "clothing/m_c_11_base_84.png" }],
|
||
"schreibweise": [],
|
||
"ueberzaehlig": ["…"], "ueberzaehligGesamt": 5337,
|
||
"unbrauchbar": [] }
|
||
```
|
||
|
||
- **Genau verglichen**, wie die öffentliche Adresse auch. Liegt etwas nur
|
||
unter einer anderen Schreibweise, steht es in `fehlt` **und** in
|
||
`schreibweise` (mit `liegtAls`) — es muss umbenannt werden, nicht neu
|
||
aufgenommen.
|
||
- `da` ist eine **Zahl**. Wer die einzelnen braucht: alle Namen ohne `fehlt`
|
||
und ohne `unbrauchbar`.
|
||
- **Keine Ordnerfessel**, wie bei `/api/exists`: die Antwort ist über `/f/`
|
||
ohnehin öffentlich.
|
||
- Endet nie auf eine Bildendung — die Proxy-Falle aus dem Abschnitt darüber
|
||
greift hier nicht.
|
||
|
||
Die Rechnung steht **einmal**, in `server/src/fehlende.ts`; die Route des
|
||
Dashboards ruft dieselbe.
|
||
|
||
---
|
||
|
||
## DELETE /api/media/<pfad>
|
||
|
||
Braucht einen Token mit dem Recht **darf löschen**.
|
||
|
||
```json
|
||
{ "deleted": "vehicles/adder.webp" }
|
||
```
|
||
|
||
Löschen ist endgültig: die Datei ist von der Platte weg, und es gibt keinen
|
||
Papierkorb. Die tägliche Sicherung hilft nur bis zum letzten Lauf.
|
||
|
||
---
|
||
|
||
## Die Wege des Dashboards
|
||
|
||
Diese brauchen eine **Sitzung** und keinen Token — sie sind für die Oberfläche
|
||
gedacht. Hier stehen sie, weil man sie auch mit `curl` und einem Cookie
|
||
benutzen kann, wenn man etwas einmalig braucht.
|
||
|
||
| Weg | Wozu |
|
||
|---|---|
|
||
| `POST /api/dash/upload-url` | Der Dienst holt die Datei selbst von einer Adresse. Siehe unten. |
|
||
| `POST /api/dash/fehlende` | Muster + Namensliste → was fehlt, was überzählig ist |
|
||
| `POST /api/dash/media/ersetzen` | Suchen und Ersetzen über Pfade. **Ohne `ausfuehren: true` wird nur gerechnet und gezeigt.** |
|
||
| `POST /api/dash/media/umbenennen` | Eine Datei; die Antwort nennt ihre Abrufzahl |
|
||
| `POST /api/dash/folders/umbenennen` | Ein Ordner mit allem darin, in einer Transaktion |
|
||
| `PATCH /api/dash/tokens/:id` | Grenzen ändern. Weggelassene Felder bleiben, `null` löscht eine Grenze. |
|
||
| `GET /api/dash/verwaltung` | Wer hat wann was eingestellt |
|
||
| `GET /api/dash/verlauf/export` | Verlauf als CSV; `?was=verwaltung` für den anderen. Semikolon + CRLF, höchstens 20 000 Zeilen. |
|
||
| `GET /api/dash/einstellungen/export` | Alle Einstellungen als JSON. **Ohne Geheimnisse**, es sei denn `?geheim=1`. |
|
||
| `POST /api/dash/einstellungen/import` | Zurück. Leere Werte überschreiben nichts. |
|
||
| `GET/POST/DELETE /api/dash/freigaben` | Nur-Lesen-Links auf einen Ordner |
|
||
| `POST /api/dash/maintenance/thumbs` \| `/webp` | Abgeleitetes nachziehen, **in Runden zu 300** |
|
||
|
||
### POST /api/dash/upload-url
|
||
|
||
```json
|
||
{ "url": "https://…/bild.png", "ordner": "items", "name": "optional.png" }
|
||
```
|
||
|
||
**Das ist die vorsichtigste Stelle im ganzen Dienst.** Ein Server, der eine vom
|
||
Benutzer genannte Adresse abruft, ist ein Angriff mit eigenem Namen — deshalb
|
||
wird geprüft, und zwar die **aufgelöste IP** und nicht der Name:
|
||
|
||
| abgelehnt | Beispiel |
|
||
|---|---|
|
||
| alles außer http/https | `file://`, `gopher://` |
|
||
| andere Anschlüsse als 80/443 | `http://host:9101/` |
|
||
| Rückschleife und Heimnetz | `127.0.0.1`, `::1`, `10.x`, `172.16–31.x`, `192.168.x`, `169.254.x`, `0.0.0.0` |
|
||
| öffentliche Namen auf private Adressen | `127.0.0.1.nip.io` |
|
||
|
||
Der letzte Punkt ist der eigentliche: ein auflösbarer Name, der auf eine private
|
||
Adresse zeigt, ist der Standardweg um jeden Namensfilter herum. **Und jede
|
||
Umleitung wird einzeln neu geprüft** — `fetch` folgen zu lassen wäre genau dort
|
||
die Lücke.
|
||
|
||
---
|
||
|
||
## Grenzen
|
||
|
||
| | |
|
||
|---|---|
|
||
| Größte Datei | 64 MB (`MAX_UPLOAD_MB`), je Token weiter einschränkbar |
|
||
| Kontingent je Token | einstellbar, ohne Grenze wenn leer |
|
||
| Pfadlänge | 200 Zeichen, höchstens 8 Ebenen |
|
||
| Anmeldeversuche | 5 frei, danach Sperre ab 30 s (verdoppelt bis 15 min) |
|
||
| Sitzung | 30 Tage |
|
||
| Papierkorb | 30 Tage **und** höchstens 20 GB (`PAPIERKORB_MAX_MB`) — was zuerst greift, greift |
|
||
|
||
Der Papierkorb hat zwei Grenzen, weil eine nicht reicht: wer 200 GB löscht,
|
||
hält sie dreißig Tage lang doppelt. Beim Ausleeren fliegt erst, was zu alt ist,
|
||
dann das **Älteste**, bis der Deckel wieder passt. Die Zahl steht in der
|
||
Antwort von `GET /api/dash/papierkorb` als `maxBytes`.
|
||
|
||
---
|
||
|
||
## Meldungen nach Discord
|
||
|
||
Vier Anlässe, alle einzeln abschaltbar: `sicherung`, `verwaiste`, `platte`,
|
||
`upload`.
|
||
|
||
**`upload` meldet nicht sofort.** Jeder Upload schiebt eine Frist von zwei
|
||
Minuten nach hinten; erst wenn es ruhig bleibt, geht **eine** Nachricht raus —
|
||
mit Anzahl, Summe, Absendern und den ersten fünf Pfaden.
|
||
|
||
Das ist keine Bequemlichkeit, sondern der Grund, warum die Funktion überhaupt
|
||
brauchbar ist: ein Serienlauf des Fotostudios legt 900 Bilder ab. 900
|
||
Nachrichten wären keine Meldung mehr, sondern ein Ausfall des Kanals — Discord
|
||
drosselt Webhooks, und wer danach eine *echte* Meldung bekommt, sieht sie nicht
|
||
mehr.
|
||
|
||
---
|
||
|
||
## Der Zwischenspeicher des Proxys
|
||
|
||
Vor dem Dienst steht Nginx Proxy Manager. Dessen `assets.conf` greift über eine
|
||
Regel auf **jede URL, die auf `.webp`, `.png`, `.js`, `.css` … endet** — und
|
||
legt die Antwort für 30 Minuten weg, dabei ignoriert sie unsere eigenen
|
||
Kopfzeilen.
|
||
|
||
Das hatte zwei sichtbare Folgen, beide nachgemessen und beide behoben:
|
||
|
||
- Ein **überschriebenes Bild** blieb bis zu 30 Minuten alt, obwohl Upload und
|
||
Datenbank „fertig" sagten.
|
||
- `/api/exists/…webp` wurde mit Antwort zwischengespeichert und danach **auch
|
||
ohne Token** ausgeliefert.
|
||
|
||
Behoben durch drei Blöcke in der *Custom Nginx Configuration* des Proxy-Hosts:
|
||
|
||
```nginx
|
||
location ^~ /api/ { proxy_cache off; include conf.d/include/proxy.conf; }
|
||
location ^~ /f/ { proxy_cache off; include conf.d/include/proxy.conf; }
|
||
location ^~ /t/ { proxy_cache off; include conf.d/include/proxy.conf; }
|
||
```
|
||
|
||
Das `^~` ist der ganze Trick: eine gewöhnliche Präfix-Regel **verliert** gegen
|
||
die Regex aus `assets.conf`. In einem Wegwerf-Nginx nachgemessen.
|
||
|
||
---
|
||
|
||
## Export
|
||
|
||
Im Dashboard unter **API** — oder direkt, mit gültiger Sitzung:
|
||
|
||
```
|
||
/api/dash/export?format=json alles, was der Dienst weiß
|
||
/api/dash/export?format=csv Semikolon und CRLF, für Excel
|
||
/api/dash/export?format=lua fertige Tabelle Name → Adresse
|
||
/api/dash/export?format=lua&folder=vehicles
|
||
```
|
||
|
||
Die Lua-Fassung liest man in einer Resource als `Urls.adder` aus.
|
||
|
||
---
|
||
|
||
*Diese Seite wird aus `docs/` im Repo erzeugt — Änderungen hier gehen beim nächsten Lauf verloren.*
|