UMBENENNEN gab es gar nicht -- verschieben ja, umbenennen nirgends. Jetzt fuer
Datei und Ordner. Die WARNUNG ist dabei der eigentliche Teil: beim Verschieben
wandert eine Datei, beim Umbenennen aendert sich IHRE ADRESSE, und die steht
womoeglich in einem Skript, das niemand mehr im Kopf hat. Der Dialog zeigt
deshalb die Abrufzahl und beim Ordner die Zahl der betroffenen Dateien, BEVOR
gedrueckt wird -- eine Datei mit viertausend Abrufen umzubenennen ist etwas
anderes als eine mit null, und der Dienst ist die einzige Stelle, die den
Unterschied kennt.
Am echten Fall geprueft, dem Kollisionsfund: items/coiloverss.png (hielt die
+-Fassung) -> items/coiloverss-plus.png, alte Adresse 404, neue 200 mit 4375 B,
Vorschau mitgewandert. Ordner: probe -> beispiele, 5 Dateien, alle Pfade in
einer Transaktion umgeschrieben.
Ein Fallstrick dabei: thumbPath haengt ".webp" an. Fuer eine Datei richtig, fuer
einen ORDNER Unsinn -- der Vorschauordner heisst thumbs/vehicles und nicht
thumbs/vehicles.webp. Dafuer gibt es jetzt moveFolder.
DIE 93 KOLLISIONEN SIND ERLEDIGT. tools/kollisionen.ps1 rechnet dieselbe
Zaehmung auf der Quelle nach und zeigt, was zusammenfaellt -- es aendert nichts,
damit die Entscheidung auf Zahlen steht. Ergebnis: 91 von 93 sind dieselbe Datei
zweimal (WEAPON_SMG.png und weapon_smg.png, byteweise gleich gross), da fehlte
nichts. Echt verloren waren ZWEI, beide mit + im Namen. Beide nachgeholt.
FIVEMANAGE-SPRACHE. POST /api/image|video|audio (v1) und POST /api/v3/file (v3),
Schluessel nackt im Authorization-Kopf. Auf einem laufenden Server stecken die
Fivemanage-Aufrufe in einem Dutzend fremder Ressourcen; sie alle umzuschreiben
tut niemand, und deshalb bliebe dieser Dienst ungenutzt daneben stehen. So ist
der Umzug eine Zeile je Skript: die Adresse.
Beide Formen sind AUS ECHTEM CODE abgelesen und nicht geraten -- fivemanage/sdk
fuer v3, Awleks/Devm-Camera fuer den aelteren Weg ueber screenshot-basic.
Gemessen: v1 -> {url,id,path}, v3 -> {status:"ok",data:{id,url}}, ohne
Schluessel 401, ein Video an /api/image -> 415 mit Grund.
Dabei bin ich in eine Falle gelaufen, vor der im eigenen Repo ein Kommentar
warnt: die Token-Wache hing an use('*'), und der Einhaengepunkt ist /api -- also
galt sie auch fuer /api/dash daneben. Das Dashboard bekam 401 auf die ANMELDUNG.
Wortwoertlich derselbe Fehler steht in upload.ts als Kommentar, weil er dort
schon einmal passiert ist. Gemerkt hat es der Gegentest, nicht der Kopf.
FREIGABE-LINKS. /s/<schluessel> zeigt einen Ordner ohne Anmeldung. Was dabei
ausdruecklich dabeisteht, in der Karte und in der Rueckfrage vor dem
Zurueckziehen: FREIGEGEBEN WIRD DIE LISTE, NICHT DER INHALT. Die Dateien sind
ohnehin oeffentlich; ein zurueckgezogener Link macht sie nicht wieder privat, er
nimmt nur die Uebersicht weg. Ohne diesen Satz zieht jemand einen Link zurueck
und glaubt, etwas sei verschwunden.
Die Antwort ist abgemessen: Name, Groesse, Art, Adresse, Laenge. NICHT Hash,
Token, Zeitpunkte, Abrufzahlen, IDs -- nichts davon braucht, wer einen Katalog
ansieht, und jedes davon waere eine Auskunft ueber den Betrieb. Ein unbekannter
Schluessel und ein zurueckgezogener geben dieselbe Antwort.
ZAHLEN, DIE SCHON DA WAREN. Sortieren nach Abrufen und nach "zuletzt geholt";
die Kachel zeigt dann auch diese Zahl statt Groesse und Datum, denn nach etwas
zu ordnen, das man nirgends sieht, ist eine Reihenfolge ohne Begruendung. Und je
Token, was damit abgelegt wurde: media.token_id wird seit dem ersten Tag
geschrieben und war NIRGENDS zu sehen. Gemessen: d4rk_photostudio haelt 810
Dateien / 99,5 MB, 3640 liegen ohne Token da.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11 KiB
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://fivemanage.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://fivemanage.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
_plateab.
Groß- und Kleinschreibung
Was über das Dashboard oder ein Werkzeug hochgeladen wird, kommt klein
an: WEAPON_SMG.png liegt als weapon_smg.png. Der Grund ist, dass ein Name
zu einer Adresse wird und Adressen sich nicht auf die Schreibweise eines
Dateisystems verlassen sollen.
Für ein Skript heißt das: die Vorlage arbeitet mit Kleinbuchstaben.
local url = ('https://fivemanage.d4rkst3r.de/f/items/%s.png'):format(item:lower())
Über /api/upload mit eigenem X-Path gilt das nicht — dort 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 seinenIf-None-Matchhin 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 → fivemanage.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.
-- vorher
exports['screenshot-basic']:requestScreenshotUpload(
'https://api.fivemanage.com/api/image', 'image',
{ headers = { Authorization = apiKey } }, cb)
-- nachher — eine Zeile
exports['screenshot-basic']:requestScreenshotUpload(
'https://fivemanage.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 für v3 und Awleks/Devm-Camera für den älteren Weg über
screenshot-basic.
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 zwei Fesseln haben:
| Fessel | Wirkung |
|---|---|
| Präfix | darf nur unterhalb dieses Pfades schreiben, z. B. vehicles/ |
| darf löschen | ohne dieses Recht antwortet DELETE mit 403 |
POST /api/upload
Nimmt die Datei in einer von drei Rumpfformen entgegen:
multipart/form-datamit dem Feldfile- Base64-Text, wenn
X-Encoding: base64gesetzt ist - 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
{
"url": "https://fivemanage.d4rkst3r.de/f/vehicles/adder.webp",
"path": "vehicles/adder.webp",
"size": 132760,
"sha256": "36cb4ddf…",
"mime": "image/webp",
"replaced": false
}
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) |
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
local TOKEN = 'd4rk_…' -- im Dashboard anlegen
local function hochladen(pfad, bytes, cb)
PerformHttpRequest('https://fivemanage.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>
Für „nur fehlende" in einem Serienlauf: eine winzige Antwort gegen 130 KB Upload.
{ "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: falseund „ich weiß es nicht" unterscheiden.
DELETE /api/media/<pfad>
Braucht einen Token mit dem Recht darf löschen.
{ "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.
Grenzen
| Größte Datei | 64 MB (MAX_UPLOAD_MB) |
| Pfadlänge | 200 Zeichen, höchstens 8 Ebenen |
| Anmeldeversuche | 5 frei, danach Sperre ab 30 s (verdoppelt bis 15 min) |
| Sitzung | 30 Tage |
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/…webpwurde mit Antwort zwischengespeichert und danach auch ohne Token ausgeliefert.
Behoben durch drei Blöcke in der Custom Nginx Configuration des Proxy-Hosts:
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.