Mugshots y screenshots de FiveM como enlaces CDN, no base64
Sube imágenes base64 desde Lua de servidor con PerformHttpRequest, guarda solo el enlace en MySQL y da a cada personaje una URL de mugshot estable.
Por Termi
- Lua
- Uploads
- MySQL
- MDT
En esta página
- Por qué no guardar base64 en la base de datos
- Consigue un token de API y déjalo en el servidor
- Subir desde Lua de servidor
- Un enlace estable por personaje
- La tabla
- Recibir la imagen desde un cliente
- Los screenshots funcionan igual
- Pasarte desde Fivemanage
- Subir directamente desde la NUI
- Errores que conviene manejar
- Preguntas frecuentes
- ¿Debo guardar imágenes base64 en MySQL?
- ¿Cómo subo un screenshot de screenshot-basic desde Lua de servidor?
- ¿Puedo poner mi API key de subida en un script de cliente o en una página NUI?
- ¿Cómo doy a cada personaje un enlace de mugshot que nunca cambie?
- ¿Funciona con FiveDock un script de subida base64 hecho para Fivemanage?
Los MDT, los DNI y las taquillas de pruebas necesitan fotos de jugadores. La primera versión suele capturar la imagen como data URI en base64 y escribir esa cadena directamente en MySQL. El primer día funciona y el día noventa duele: el base64 pesa un tercio más que la imagen, cada consulta que selecciona la columna lo arrastra, y los dumps de tu base de datos crecen con cada arresto.
La solución es subir la imagen una vez y guardar solo su enlace. Esta guía lo hace desde Lua de servidor con PerformHttpRequest y el endpoint base64 de FiveDock, que acepta la misma petición que el de Fivemanage.
Por qué no guardar base64 en la base de datos
Consigue un token de API y déjalo en el servidor
Las subidas necesitan un token de API. Créalo en el panel, en la página Tokens, con el scope files:write; empieza por fd_live_. No es la clave de agente (fda_live_…) que usa el resource fivedock, y la API de subida no acepta claves de agente.
Pon el token en server.cfg con set. Nunca con setr, que replica el valor a todos los clientes, ni con sets, que lo publica en la información del servidor:
# the API token, server-only: set, never setr or sets
set fivedock_api_token "fd_live_…"Si un cliente tiene que subir por sí mismo, mira más abajo subir directamente desde la NUI.
Subir desde Lua de servidor
POST https://api.fivedock.dev/api/v3/file/base64 recibe un JSON con la imagen como data URI, un nombre de archivo y, opcionalmente, una key. El enlace vuelve en 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"
}
}Este es un script de servidor completo. Mantiene la forma de una integración típica con Fivemanage y añade la 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)El token va tal cual en la cabecera Authorization; Bearer fd_live_… también funciona. Cuando la subida sale bien, solo el enlace llega a MySQL.
Un enlace estable por personaje
La key es lo que mantiene estable el enlace. Sin ella, cada subida recibe un enlace aleatorio nuevo y la imagen anterior sigue ocupando espacio. Con mugshots/<identifier>.webp:
Los enlaces con key se cachean cinco minutos, así que un mugshot nuevo llega a todas las pantallas en unos cinco minutos.
Reglas de las keys
Las keys pueden contener letras, dígitos y . _ - ~ @ + : ( ) , = !, con / entre carpetas. Una key explícita que incumple las reglas se rechaza con 400, por eso el script limpia antes el identificador. El tipo de archivo se detecta por los bytes, no por el nombre, pero mantén la extensión honesta de todos modos: si tu captura produce JPEG, usa .jpg en la key y en el nombre de archivo.
La tabla
ON DUPLICATE KEY UPDATE solo funciona cuando identifier es clave primaria o única. Y como ahora la fila guarda un enlace, un VARCHAR de sobra:
CREATE TABLE IF NOT EXISTS `mdt_mugshots` (
`identifier` VARCHAR(64) NOT NULL,
`image` VARCHAR(512) NOT NULL,
PRIMARY KEY (`identifier`)
);El script usa oxmysql, así que el resource carga su librería antes que el script:
fx_version 'cerulean'
game 'gta5'
lua54 'yes'
server_scripts {
'@oxmysql/lib/MySQL.lua',
'server/mugshots.lua',
}Recibir la imagen desde un cliente
Los mugshots suelen capturarse en el cliente (a menudo un headshot de ped dibujado en un canvas dentro de la NUI) y enviarse al servidor. Trata lo que llega como no fiable: decide en el servidor quién puede hacer mugshots, saca el identificador del jugador en el servidor y descarta todo lo que no sea una imagen razonablemente pequeña:
-- 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 y getCharacterId dependen de tu framework, así que te toca escribirlas a ti. Pasar una cadena grande por un net event tampoco sale gratis; si tus imágenes pasan de unos cientos de kilobytes, súbelas desde la NUI, como se muestra abajo.
Los screenshots funcionan igual
Con screenshot-basic, el servidor puede pedirle un screenshot a un cliente y recibe una data URI, que es justo lo que acepta el endpoint base64. Aquí como fotos de pruebas, una key por caso y marca de tiempo:
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 tienes el agente de FiveDock, exports.fivedock:screenshot hace el screenshot y lo sube en una sola llamada, sin token en tu script (también necesita screenshot-basic iniciado). Mira la documentación del agente:
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)Pasarte desde Fivemanage
Si tu MDT ya sube a Fivemanage, el cambio es pequeño:
-- 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 petición y el data.url de la respuesta son los mismos, así que el resto de tu código se queda como está. Para los enlaces que ya están en tu base de datos, la guía de migración copia tus archivos de Fivemanage a FiveDock y te da una lista de enlace antiguo → enlace nuevo para actualizar las filas.
Subir directamente desde la NUI
A veces la imagen nace en el navegador: una app de cámara en un resource de móvil, o una captura de canvas. No le des tu token a la página NUI, porque cualquier jugador puede leer todo lo que contiene. Deja que el servidor genere un enlace presigned y entrégale al cliente solo ese. El enlace no necesita token, se puede limitar a una subida, un tipo de archivo y una key, y caduca solo:
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;
}Como el servidor eligió la key, ya conoce el enlace final y puede guardarlo sin fiarse de lo que le devuelva el cliente. La caducidad, los límites de tamaño y el resto de opciones están en la referencia de la API.
Errores que conviene manejar
No reintentes en bucle cerrado ante los errores. Un mugshot fallido puede esperar a la próxima vez que un agente haga uno; un script que reintenta sin parar, no.
Preguntas frecuentes
¿Debo guardar imágenes base64 en MySQL?
No. Una cadena base64 pesa aproximadamente un tercio más que la imagen que codifica. Cada consulta que selecciona la columna la arrastra, y el dump de la base de datos crece con cada imagen. Sube la imagen una vez a un CDN y guarda solo su enlace, que cabe en un VARCHAR(512).
¿Cómo subo un screenshot de screenshot-basic desde Lua de servidor?
En el servidor, llama a requestClientScreenshot de screenshot-basic sin la opción fileName, y el callback recibe el screenshot como data URI. Envíalo como JSON (base64, filename y una key opcional) a POST https://api.fivedock.dev/api/v3/file/base64 con tu token de API en la cabecera Authorization. El enlace del CDN vuelve en data.url, y ese enlace es lo que guardas.
¿Puedo poner mi API key de subida en un script de cliente o en una página NUI?
No. Cualquier jugador puede leer los scripts de cliente y todo lo que contiene una página NUI, así que quien encuentre la key puede subir archivos a tu organización. Deja el token en server.cfg con set y sube desde Lua de servidor. Si la imagen nace en la NUI, haz que el servidor genere un enlace presigned para una sola subida y entrega al cliente solo ese enlace.
¿Cómo doy a cada personaje un enlace de mugshot que nunca cambie?
Envía una key con la subida, por ejemplo mugshots/<identifier>.webp. El archivo se sirve entonces en https://cdn.fivedock.dev/o/<your-workspace>/mugshots/<identifier>.webp, y un mugshot nuevo subido a la misma key reemplaza la imagen detrás de ese enlace. Los enlaces con key se cachean cinco minutos, así que la imagen nueva se ve en todas partes en unos cinco minutos.
¿Funciona con FiveDock un script de subida base64 hecho para Fivemanage?
Sí. FiveDock responde a la misma petición POST /api/v3/file/base64 en https://api.fivedock.dev y devuelve el enlace en el mismo campo data.url. Cambia la URL y sustituye la key de Fivemanage por un token de API de FiveDock (fd_live_…) con el scope files:write; el resto del script se queda como está.