Mugshots et screenshots FiveM en liens CDN, pas en base64
Envoyez des images base64 depuis le Lua serveur avec PerformHttpRequest, gardez seulement le lien en MySQL et donnez à chaque perso une URL de mugshot stable.
Par Termi
- Lua
- Uploads
- MySQL
- MDT
Sur cette page
- Pourquoi ne pas garder le base64 en base
- Créer un token API et le garder côté serveur
- Uploader depuis du Lua côté serveur
- Un lien stable par personnage
- La table
- Recevoir l’image depuis un client
- Les screenshots fonctionnent pareil
- Quitter Fivemanage
- Uploader directement depuis la NUI
- Les erreurs à gérer
- Questions fréquentes
- Faut-il stocker des images base64 dans MySQL ?
- Comment uploader un screenshot de screenshot-basic depuis du Lua serveur ?
- Puis-je mettre ma clé API d’upload dans un script client ou une page NUI ?
- Comment donner à chaque personnage un lien de mugshot qui ne change jamais ?
- Un script d’upload base64 pour Fivemanage fonctionne-t-il avec FiveDock ?
Les MDT, les cartes d’identité et les casiers à preuves ont tous besoin de photos de joueurs. La première version capture en général l’image en data URI base64 et écrit cette chaîne directement dans MySQL. Ça marche le premier jour et ça fait mal le quatre-vingt-dixième : le base64 pèse un tiers de plus que l’image, chaque requête qui sélectionne la colonne la traîne avec elle, et vos dumps de base grossissent à chaque arrestation.
La solution : envoyer l’image une fois et ne stocker que son lien. Ce guide le fait depuis du Lua côté serveur, avec PerformHttpRequest et l’endpoint base64 de FiveDock, qui accepte la même requête que celui de Fivemanage.
Pourquoi ne pas garder le base64 en base
Créer un token API et le garder côté serveur
Les uploads demandent un token API. Créez-le dans le panel, sur la page Jetons, avec le scope files:write ; il commence par fd_live_. Ce n’est pas la clé d’agent (fda_live_…) qu’utilise la resource fivedock, et l’API d’upload n’accepte pas les clés d’agent.
Mettez le token dans server.cfg avec set. Jamais avec setr, qui réplique la valeur à chaque client, ni sets, qui la publie dans les infos du serveur :
# the API token, server-only: set, never setr or sets
set fivedock_api_token "fd_live_…"Si un client doit uploader lui-même, voyez plus bas l’upload direct depuis la NUI.
Uploader depuis du Lua côté serveur
POST https://api.fivedock.dev/api/v3/file/base64 prend du JSON avec l’image en data URI, un nom de fichier et, en option, une clé. Le lien revient dans data.url :
{
"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"
}
}Voici un script serveur complet. Il garde la forme d’une intégration Fivemanage classique et ajoute la clé :
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)Le token va tel quel dans l’en-tête Authorization ; Bearer fd_live_… fonctionne aussi. Quand l’upload réussit, seul le lien arrive dans MySQL.
Un lien stable par personnage
C’est la key qui rend le lien stable. Sans elle, chaque upload reçoit un nouveau lien aléatoire et l’ancienne image continue d’occuper de la place. Avec mugshots/<identifier>.webp :
Les liens à clé sont mis en cache cinq minutes : un nouveau mugshot atteint tous les écrans en cinq minutes environ.
Règles des clés
Une clé peut contenir des lettres, des chiffres et . _ - ~ @ + : ( ) , = !, avec / entre les dossiers. Une clé explicite qui enfreint ces règles est refusée avec un 400, c’est pourquoi le script nettoie d’abord l’identifiant. Le type de fichier est détecté à partir des octets, pas du nom, mais gardez quand même une extension honnête : si votre capture produit du JPEG, mettez .jpg dans la clé et dans le nom de fichier.
La table
ON DUPLICATE KEY UPDATE ne marche que si identifier est une clé primaire ou unique. Et comme la ligne contient maintenant un lien, un VARCHAR suffit largement :
CREATE TABLE IF NOT EXISTS `mdt_mugshots` (
`identifier` VARCHAR(64) NOT NULL,
`image` VARCHAR(512) NOT NULL,
PRIMARY KEY (`identifier`)
);Le script utilise oxmysql, la resource charge donc sa bibliothèque avant le script :
fx_version 'cerulean'
game 'gta5'
lua54 'yes'
server_scripts {
'@oxmysql/lib/MySQL.lua',
'server/mugshots.lua',
}Recevoir l’image depuis un client
Les mugshots sont en général capturés côté client (souvent un headshot de ped dessiné sur un canvas dans la NUI) puis envoyés au serveur. Considérez tout ce qui arrive comme non fiable : décidez côté serveur qui peut prendre des mugshots, prenez l’identifiant du joueur côté serveur, et jetez tout ce qui n’est pas une image raisonnablement petite :
-- 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 et getCharacterId dépendent de votre framework, c’est à vous de les écrire. Faire passer une grosse chaîne dans un net event n’est pas gratuit non plus ; si vos images dépassent quelques centaines de kilo-octets, uploadez-les depuis la NUI, comme montré plus bas.
Les screenshots fonctionnent pareil
Avec screenshot-basic, le serveur peut demander un screenshot à un client et reçoit une data URI, exactement ce que prend l’endpoint base64. Ici sous forme de photos de preuves, une clé par dossier et par horodatage :
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)
endSi vous faites tourner l’agent FiveDock, exports.fivedock:screenshot prend le screenshot et l’envoie en un seul appel, sans token dans votre script (screenshot-basic doit aussi être démarré). Voir la doc de l’agent :
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)Quitter Fivemanage
Si votre MDT envoie déjà ses fichiers vers Fivemanage, le changement est minime :
-- 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', '')La requête et le data.url de la réponse sont les mêmes, le reste de votre code ne bouge donc pas. Pour les liens déjà présents dans votre base, le guide de migration copie vos fichiers Fivemanage vers FiveDock et vous donne une liste ancien lien → nouveau lien pour mettre les lignes à jour.
Uploader directement depuis la NUI
Parfois l’image naît dans le navigateur : une appli appareil photo dans une resource de téléphone, ou une capture de canvas. Ne donnez pas votre token à la page NUI, car chaque joueur peut lire tout ce qu’elle contient. Laissez le serveur générer un lien presigned et ne transmettez que celui-là au client. Le lien n’a pas besoin de token, peut être limité à un upload, un type de fichier et une clé, et expire tout seul :
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;
}Comme le serveur a choisi la clé, il connaît déjà le lien final et peut le stocker sans se fier à ce que le client lui renvoie. L’expiration, les limites de taille et les autres options sont dans la référence de l’API.
Les erreurs à gérer
Ne réessayez pas en boucle serrée après une erreur. Un mugshot raté peut attendre la prochaine fois qu’un officier en prend un ; un script qui réessaie sans fin, non.
Questions fréquentes
Faut-il stocker des images base64 dans MySQL ?
Non. Une chaîne base64 pèse environ un tiers de plus que l’image qu’elle encode. Chaque requête qui sélectionne la colonne la transporte, et le dump de la base grossit à chaque image. Envoyez l’image une fois sur un CDN et ne stockez que son lien, qui tient dans un VARCHAR(512).
Comment uploader un screenshot de screenshot-basic depuis du Lua serveur ?
Côté serveur, appelez requestClientScreenshot de screenshot-basic sans option fileName, et le callback reçoit le screenshot sous forme de data URI. Envoyez-le en JSON (base64, filename et une key optionnelle) à POST https://api.fivedock.dev/api/v3/file/base64, avec votre token API dans l’en-tête Authorization. Le lien CDN revient dans data.url, et c’est ce lien que vous stockez.
Puis-je mettre ma clé API d’upload dans un script client ou une page NUI ?
Non. Chaque joueur peut lire les scripts client et tout le contenu d’une page NUI : quiconque trouve la clé peut uploader dans votre organisation. Gardez le token dans server.cfg avec set et uploadez depuis du Lua serveur. Si l’image naît dans la NUI, faites générer par le serveur un lien presigned pour un seul upload et ne transmettez que ce lien au client.
Comment donner à chaque personnage un lien de mugshot qui ne change jamais ?
Envoyez une key avec l’upload, par exemple mugshots/<identifier>.webp. Le fichier est alors servi sur https://cdn.fivedock.dev/o/<your-workspace>/mugshots/<identifier>.webp, et un nouveau mugshot envoyé sur la même clé remplace l’image derrière ce lien. Les liens à clé sont mis en cache cinq minutes, la nouvelle image apparaît donc partout en cinq minutes environ.
Un script d’upload base64 pour Fivemanage fonctionne-t-il avec FiveDock ?
Oui. FiveDock répond à la même requête POST /api/v3/file/base64 sur https://api.fivedock.dev et renvoie le lien dans le même champ data.url. Changez l’URL et remplacez la clé Fivemanage par un token API FiveDock (fd_live_…) avec le scope files:write ; le reste du script ne bouge pas.