FiveM Mugshots und Screenshots als CDN-Link statt base64
Lade base64-Bilder per PerformHttpRequest aus Server-Lua hoch, speichere nur den Link in MySQL und gib jedem Charakter eine feste Mugshot-URL.
Von Termi
- Lua
- Uploads
- MySQL
- MDT
Auf dieser Seite
- Warum base64 nicht in die Datenbank gehört
- API-Token holen und auf dem Server lassen
- Upload aus Server-Lua
- Ein stabiler Link pro Charakter
- Die Tabelle
- Das Bild von einem Client empfangen
- Screenshots funktionieren genauso
- Von Fivemanage umziehen
- Upload direkt aus der NUI
- Fehler, die du behandeln solltest
- Häufige Fragen
- Sollte ich base64-Bilder in MySQL speichern?
- Wie lade ich einen screenshot-basic-Screenshot aus Server-Lua hoch?
- Darf ich meinen Upload-API-Key in ein Client-Script oder eine NUI-Seite schreiben?
- Wie gebe ich jedem Charakter einen Mugshot-Link, der sich nie ändert?
- Funktioniert ein Fivemanage-base64-Upload-Script mit FiveDock?
MDTs, Ausweise und Beweisschränke brauchen Fotos von Spielern. Die erste Version schießt das Bild meist als base64-Data-URI und schreibt den String direkt in MySQL. Am ersten Tag klappt das, am neunzigsten tut es weh: base64 ist ein Drittel größer als das Bild selbst, jede Query, die die Spalte selektiert, schleppt es mit, und deine Datenbank-Dumps wachsen mit jeder Festnahme.
Die Lösung: das Bild einmal hochladen und nur den Link speichern. Diese Anleitung macht das aus Server-Lua mit PerformHttpRequest und dem base64-Endpoint von FiveDock, der denselben Request annimmt wie der von Fivemanage.
Warum base64 nicht in die Datenbank gehört
API-Token holen und auf dem Server lassen
Uploads brauchen einen API-Token. Erstell ihn im Panel auf der Seite Tokens mit dem Scope files:write; er beginnt mit fd_live_. Das ist nicht der Agent-Key (fda_live_…), den die Resource fivedock benutzt, und die Upload-API akzeptiert keine Agent-Keys.
Trag den Token mit set in die server.cfg ein. Nimm niemals setr, das den Wert an jeden Client repliziert, und auch nicht sets, das ihn in den Serverinfos veröffentlicht:
# the API token, server-only: set, never setr or sets
set fivedock_api_token "fd_live_…"Wenn ein Client selbst hochladen muss, schau unten bei Upload direkt aus der NUI.
Upload aus Server-Lua
POST https://api.fivedock.dev/api/v3/file/base64 nimmt JSON mit dem Bild als Data-URI, einem Dateinamen und optional einem Key. Der Link kommt in data.url zurück:
{
"base64": "data:image/webp;base64,UklGR…",
"filename": "char1_abc123.webp",
"key": "mugshots/char1_abc123.webp"
}{
"data": {
"url": "https://cdn.fivedock.dev/o/my-city/mugshots/char1_abc123.webp"
}
}Hier ein vollständiges Server-Script. Es behält die Form einer typischen Fivemanage-Integration und ergänzt den Key:
local API_URL = 'https://api.fivedock.dev/api/v3/file/base64'
local API_TOKEN = GetConvar('fivedock_api_token', '')
if API_TOKEN == '' then
print('^1fivedock_api_token is not set, mugshot uploads will fail^0')
end
-- "char1:abc123" becomes "char1_abc123": keep letters, digits, _ and -
local function cleanId(value)
return (tostring(value):gsub('[^%w_%-]', '_'))
end
local function saveMugshot(identifier, mugshotImage)
local cleanIdentifier = cleanId(identifier)
PerformHttpRequest(API_URL, function(statusCode, text, headers, errorData)
if statusCode == 200 or statusCode == 201 then
local data = json.decode(text)
if data and data.data and data.data.url then
MySQL.insert.await('INSERT INTO `mdt_mugshots` (`identifier`, `image`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `image` = VALUES(`image`)', { identifier, data.data.url })
end
else
print(('Error uploading mugshot (%s): %s'):format(statusCode, tostring(errorData or text)))
end
end, 'POST', json.encode({
base64 = mugshotImage,
filename = cleanIdentifier .. '.webp',
key = 'mugshots/' .. cleanIdentifier .. '.webp',
}), {
['Content-Type'] = 'application/json',
['Authorization'] = API_TOKEN,
})
end
exports('saveMugshot', saveMugshot)Der Token kommt unverändert in den Authorization-Header; Bearer fd_live_… funktioniert auch. Nach einem erfolgreichen Upload landet nur der Link in MySQL.
Ein stabiler Link pro Charakter
Der key hält den Link stabil. Ohne ihn bekommt jeder Upload einen neuen zufälligen Link, und das alte Bild belegt weiter Platz. Mit mugshots/<identifier>.webp gilt:
Keyed Links werden fünf Minuten gecached, ein neues Mugshot erscheint also nach etwa fünf Minuten überall.
Regeln für Keys
Keys dürfen Buchstaben, Ziffern und . _ - ~ @ + : ( ) , = ! enthalten, mit / zwischen Ordnern. Ein expliziter Key, der die Regeln verletzt, wird mit 400 abgelehnt, deshalb säubert das Script den Identifier vorher. Dateitypen werden an den Bytes erkannt, nicht am Namen, aber halte die Endung trotzdem korrekt: Liefert deine Aufnahme JPEG, nimm .jpg im Key und im Dateinamen.
Die Tabelle
ON DUPLICATE KEY UPDATE funktioniert nur, wenn identifier ein Primary oder Unique Key ist. Und weil die Zeile jetzt einen Link enthält, reicht ein VARCHAR völlig:
CREATE TABLE IF NOT EXISTS `mdt_mugshots` (
`identifier` VARCHAR(64) NOT NULL,
`image` VARCHAR(512) NOT NULL,
PRIMARY KEY (`identifier`)
);Das Script nutzt oxmysql, die Resource lädt deshalb dessen Library vor dem Script:
fx_version 'cerulean'
game 'gta5'
lua54 'yes'
server_scripts {
'@oxmysql/lib/MySQL.lua',
'server/mugshots.lua',
}Das Bild von einem Client empfangen
Mugshots entstehen meist auf dem Client (oft ein Ped-Headshot, der in der NUI auf ein Canvas gezeichnet wird) und gehen dann an den Server. Behandle alles, was ankommt, als nicht vertrauenswürdig: Entscheide auf dem Server, wer Mugshots machen darf, nimm den Identifier vom serverseitigen Spieler und wirf alles weg, was kein halbwegs kleines Bild ist:
-- about 1.5 MB of image once decoded; a headshot is a few dozen KB
local MAX_IMAGE_LENGTH = 2 * 1024 * 1024
RegisterNetEvent('mdt:saveMugshot', function(targetId, mugshotImage)
local src = source
-- yours to write: e.g. is the sender on duty as police?
if not canTakeMugshots(src) then return end
if type(mugshotImage) ~= 'string'
or #mugshotImage > MAX_IMAGE_LENGTH
or not mugshotImage:find('^data:image/') then
return
end
-- yours to write: ESX xPlayer.identifier, QBCore Player.PlayerData.citizenid
local identifier = getCharacterId(targetId)
if identifier then
saveMugshot(identifier, mugshotImage)
end
end)canTakeMugshots und getCharacterId hängen von deinem Framework ab, die musst du selbst schreiben. Auch einen großen String durch ein Net-Event zu schieben ist nicht gratis; sind deine Bilder größer als ein paar hundert Kilobyte, lade sie wie unten gezeigt aus der NUI hoch.
Screenshots funktionieren genauso
Mit screenshot-basic kann der Server einen Client um einen Screenshot bitten und bekommt eine Data-URI zurück, genau das, was der base64-Endpoint annimmt. Hier als Beweisfotos, ein Key pro Fall und Zeitstempel:
local function saveEvidence(playerId, caseId)
exports['screenshot-basic']:requestClientScreenshot(playerId, {
encoding = 'webp',
quality = 0.85,
}, function(err, data)
if err then
print(('Screenshot failed: %s'):format(err))
return
end
local key = ('evidence/%s/%d.webp'):format(cleanId(caseId), os.time())
PerformHttpRequest(API_URL, function(statusCode, text, headers, errorData)
if statusCode ~= 200 and statusCode ~= 201 then
print(('Error uploading evidence (%s): %s'):format(statusCode, tostring(errorData or text)))
return
end
local body = json.decode(text)
local url = body and body.data and body.data.url
if url then
MySQL.insert.await('INSERT INTO `mdt_evidence` (`case_id`, `image`) VALUES (?, ?)', { caseId, url })
end
end, 'POST', json.encode({ base64 = data, filename = 'evidence.webp', key = key }), {
['Content-Type'] = 'application/json',
['Authorization'] = API_TOKEN,
})
end)
endWenn du den FiveDock-Agent laufen hast, macht exports.fivedock:screenshot Screenshot und Upload in einem Aufruf, ohne Token in deinem Script (screenshot-basic muss dafür ebenfalls gestartet sein). Mehr in den Agent-Docs:
exports.fivedock:screenshot(playerId, { folder = '/evidence', encoding = 'webp', quality = 0.85 }, function(err, file)
if err then return print(err) end
print(file.url)
end)Von Fivemanage umziehen
Wenn dein MDT schon zu Fivemanage hochlädt, ist die Änderung klein:
-- was: "https://api.fivemanage.com/api/v3/file/base64"
local url = "https://api.fivedock.dev/api/v3/file/base64"
-- was: your Fivemanage API key
local apiKey = GetConvar('fivedock_api_token', '')Request und data.url in der Antwort sind gleich, der Rest deines Codes bleibt also, wie er ist. Für Links, die schon in deiner Datenbank stehen, kopiert die Migrationsanleitung deine Fivemanage-Dateien zu FiveDock und gibt dir eine Liste alter Link → neuer Link, mit der du die Zeilen aktualisierst.
Upload direkt aus der NUI
Manchmal entsteht das Bild im Browser: eine Kamera-App in einer Phone-Resource oder ein Canvas-Capture. Gib der NUI-Seite nicht deinen Token, denn jeder Spieler kann alles lesen, was eine NUI-Seite enthält. Lass den Server einen Presigned-Link erzeugen und gib nur den an den Client. Der Link braucht keinen Token, kann auf einen Upload, einen Dateityp und einen Key begrenzt werden und läuft von selbst ab:
local PRESIGN_URL = 'https://api.fivedock.dev/api/v3/file/presigned-url'
RegisterNetEvent('mdt:requestMugshotLink', function(targetId)
local src = source
if not canTakeMugshots(src) then return end
local identifier = getCharacterId(targetId)
if not identifier then return end
local query = ('?singleUse=true&fileType=image&key=mugshots/%s.webp'):format(cleanId(identifier))
PerformHttpRequest(PRESIGN_URL .. query, function(statusCode, text)
if statusCode ~= 200 then return end
local body = json.decode(text)
TriggerClientEvent('mdt:mugshotLink', src, body.data.presignedUrl)
end, 'GET', '', { ['Authorization'] = API_TOKEN })
end)// NUI: upload a canvas with the one-time link the server sent
async function uploadMugshot(presignedUrl, canvas) {
const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/webp', 0.85));
const body = new FormData();
body.append('file', blob, 'mugshot.webp');
const response = await fetch(presignedUrl, { method: 'POST', body });
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
return (await response.json()).data.url;
}Weil der Server den Key gewählt hat, kennt er den fertigen Link schon und kann ihn speichern, ohne dem zu vertrauen, was der Client zurückmeldet. Ablauf, Größenlimits und die übrigen Optionen stehen in der API-Referenz.
Fehler, die du behandeln solltest
Wiederhole bei Fehlern nicht in einer engen Schleife. Ein fehlgeschlagenes Mugshot kann auf das nächste Mal warten, wenn ein Officer eins macht; ein Script, das ewig wiederholt, kann das nicht.
Häufige Fragen
Sollte ich base64-Bilder in MySQL speichern?
Nein. Ein base64-String ist etwa ein Drittel größer als das Bild, das er kodiert. Jede Query, die die Spalte selektiert, trägt ihn mit, und der Datenbank-Dump wächst mit jedem Bild. Lade das Bild einmal auf ein CDN hoch und speichere nur den Link, der in ein VARCHAR(512) passt.
Wie lade ich einen screenshot-basic-Screenshot aus Server-Lua hoch?
Ruf auf dem Server requestClientScreenshot von screenshot-basic ohne die Option fileName auf, dann bekommt der Callback den Screenshot als Data-URI. Schick ihn als JSON (base64, filename und ein optionaler key) per POST https://api.fivedock.dev/api/v3/file/base64 mit deinem API-Token im Authorization-Header. Der CDN-Link kommt in data.url zurück, und diesen Link speicherst du.
Darf ich meinen Upload-API-Key in ein Client-Script oder eine NUI-Seite schreiben?
Nein. Jeder Spieler kann Client-Scripts und alles lesen, was eine NUI-Seite enthält, also kann jeder, der den Key findet, in deine Organisation hochladen. Lass den Token mit set in der server.cfg und lade aus Server-Lua hoch. Entsteht das Bild in der NUI, lass den Server einen Presigned-Link für einen Upload erzeugen und gib nur diesen Link an den Client.
Wie gebe ich jedem Charakter einen Mugshot-Link, der sich nie ändert?
Schick beim Upload einen key mit, etwa mugshots/<identifier>.webp. Die Datei wird dann unter https://cdn.fivedock.dev/o/<your-workspace>/mugshots/<identifier>.webp ausgeliefert, und ein neues Mugshot auf demselben Key ersetzt das Bild hinter diesem Link. Keyed Links werden fünf Minuten gecached, das neue Bild erscheint also nach etwa fünf Minuten überall.
Funktioniert ein Fivemanage-base64-Upload-Script mit FiveDock?
Ja. FiveDock beantwortet denselben POST /api/v3/file/base64-Request auf https://api.fivedock.dev und liefert den Link im selben Feld data.url. Tausch die URL aus und ersetze den Fivemanage-Key durch einen FiveDock-API-Token (fd_live_…) mit dem Scope files:write; der Rest des Scripts bleibt, wie er ist.