fix(thumbnail): serve image bytes directly + on-demand thumb generation with Wasabi backfill
continuous-integration/webhook Falha no deploy (VPS4)

O endpoint /api/images/:id/thumbnail retornava JSON {url}, mas o React aponta
<img src> direto para ele, causando placeholders cinza (sem erro no devtools).
Agora serve image/jpeg diretamente. Quando thumb_filename é null, puxa a original,
gera a thumb na hora, serve imediatamente e faz backfill no Wasabi + atualiza o banco.
This commit is contained in:
VPS 4 Deploy Agent
2026-05-30 18:48:25 +02:00
parent 1a5ac83f53
commit 12891da889
2 changed files with 41 additions and 17 deletions
@@ -1 +1 @@
2.1.14
2.1.15
+40 -16
View File
@@ -524,32 +524,56 @@ app.use(checkInstallationMiddleware);
// Middleware que pula autenticação para rotas públicas de imagens
const publicImageRoutes = express.Router();
// Serve os BYTES da thumbnail diretamente (image/jpeg), para uso direto em <img src>.
// Se a imagem ainda não tem thumbnail, gera na hora a partir da original,
// serve imediatamente e faz backfill (upload pro Wasabi + atualiza banco) em background.
publicImageRoutes.get('/:id/thumbnail', async (req, res) => {
try {
const image = await db.get('SELECT thumb_filename, filename, client_name, patient_name FROM images WHERE id = $1', [req.params.id]);
const image = await db.get(
'SELECT id, thumb_filename, filename, client_name, patient_name FROM images WHERE id = $1',
[req.params.id]
);
if (!image) return res.status(404).json({ error: 'Imagem não encontrada' });
const fileToCheck = image.thumb_filename || image.filename;
let url;
res.setHeader('Cache-Control', 'public, max-age=86400');
const fsSync = require('fs');
const path = require('path');
const localPath = path.join(process.env.UPLOAD_DIR || '/app/uploads', fileToCheck);
if (fsSync.existsSync(localPath)) {
url = `/uploads/${fileToCheck}`;
} else {
// 1. Já existe thumbnail — serve direto (local ou Wasabi)
if (image.thumb_filename) {
try {
const wasabiUrl = await storage.getImageUrl(fileToCheck, image.client_name, image.patient_name, false);
url = wasabiUrl || `/uploads/${fileToCheck}`;
const buf = await storage.getImageBuffer(image.thumb_filename, image.client_name, image.patient_name);
if (buf) {
res.setHeader('Content-Type', 'image/jpeg');
return res.end(buf);
}
} catch (e) {
console.error('Erro ao gerar URL Wasabi para thumbnail:', e);
url = `/uploads/${fileToCheck}`;
console.error(`⚠️ [thumbnail] Falha ao ler thumb existente (id ${image.id}):`, e.message);
// cai para regeneração abaixo
}
}
res.json({ url });
// 2. Sem thumbnail — puxa a original, gera a thumb na hora
const originalBuf = await storage.getImageBuffer(image.filename, image.client_name, image.patient_name);
if (!originalBuf) {
return res.status(404).json({ error: 'Imagem original não encontrada' });
}
const thumbBuf = await imageProcessor.getThumbnail(originalBuf, 500, 500);
// Serve imediatamente para não travar a UI
res.setHeader('Content-Type', 'image/jpeg');
res.end(thumbBuf);
// Backfill em background: salva a thumb no Wasabi e atualiza o banco
const thumbFilename = `thumb_${image.filename.replace(/\.[^.]+$/, '')}.jpg`;
storage.saveImage(thumbFilename, thumbBuf, image.client_name, image.patient_name, false)
.then(() => db.run('UPDATE images SET thumb_filename = $1 WHERE id = $2', [thumbFilename, image.id]))
.then(() => console.log(`✅ [thumb-backfill] Thumb gerada/salva: ${thumbFilename} (id ${image.id})`))
.catch(err => console.error(`⚠️ [thumb-backfill] Falha id ${image.id}:`, err.message));
} catch (e) {
res.status(500).json({ error: e.message });
console.error(`❌ [thumbnail] Erro id ${req.params.id}:`, e.message);
if (!res.headersSent) res.status(500).json({ error: e.message });
}
});