fix(thumbnail): corrige 500 em RX sem thumb — fallback Wasabi legado
continuous-integration/webhook Deploy concluído (VPS4)
continuous-integration/webhook Deploy concluído (VPS4)
Causa raiz: request-presigned-urls usava firstName/lastName para montar o patientName do caminho no Wasabi, mas o client envia apenas name. Resultado: toda imagem era salva em '.../Desconhecido/arquivo.png' no Wasabi, mas o banco guardava o nome real do paciente — na recuperação, a key ficava diferente e resultava em NoSuchKey → 500. Correções: 1. server.js: getImageBufferWithFallback() — tenta o path correto (nome real) e, se falhar, tenta o path legado 'Desconhecido'. Usado tanto para ler thumb existente quanto para gerar do original. 2. server.js: patientName corrigido em send-image e request-presigned-urls para usar data.patientData.name (o campo que o client realmente envia), garantindo consistência entre Wasabi e banco em novos uploads. 3. routes/images.js: runThumbBackfill agora usa storage.getImageBuffer com fallback 'Desconhecido' em vez de leitura apenas do disco local — o boot backfill passa a funcionar para imagens no Wasabi. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -540,23 +540,32 @@ router.post('/backfill-thumbs', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function runThumbBackfill(rows) {
|
async function runThumbBackfill(rows) {
|
||||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, '..', '..', 'uploads');
|
|
||||||
const PROCESSED_DIR = process.env.PROCESSED_DIR || path.join(__dirname, '..', '..', 'processed');
|
|
||||||
let done = 0, failed = 0;
|
let done = 0, failed = 0;
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
try {
|
try {
|
||||||
// Tenta carregar a imagem do disco local
|
// Busca a original via storage (disco local → Wasabi com path correto → fallback 'Desconhecido')
|
||||||
let imgBuffer = null;
|
let imgBuffer = null;
|
||||||
for (const dir of [UPLOAD_DIR, PROCESSED_DIR]) {
|
try {
|
||||||
try { imgBuffer = await fs.readFile(path.join(dir, row.filename)); break; } catch (_) {}
|
imgBuffer = await storage.getImageBuffer(row.filename, row.clinic_name, row.client_name, row.patient_name);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Fallback legado: uploads antigos foram gravados com patientName='Desconhecido' no Wasabi
|
||||||
|
if (!imgBuffer && row.patient_name !== 'Desconhecido') {
|
||||||
|
try {
|
||||||
|
imgBuffer = await storage.getImageBuffer(row.filename, row.clinic_name, row.client_name, 'Desconhecido');
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!imgBuffer) { failed++; continue; }
|
if (!imgBuffer) { failed++; continue; }
|
||||||
|
|
||||||
const thumbBuffer = await imageProcessor.getThumbnail(imgBuffer);
|
const thumbBuffer = await imageProcessor.getThumbnail(imgBuffer);
|
||||||
const thumbFilename = `thumb_${row.filename.replace(/\.[^.]+$/, '')}.jpg`;
|
const thumbFilename = `thumb_${row.filename.replace(/\.[^.]+$/, '')}.jpg`;
|
||||||
await storage.saveImage(thumbFilename, thumbBuffer, row.clinic_name, row.client_name, row.patient_name, false);
|
await storage.saveImage(thumbFilename, thumbBuffer, row.clinic_name, row.client_name, row.patient_name, false);
|
||||||
await db.run('UPDATE images SET thumb_filename = $1 WHERE id = $2', [thumbFilename, row.id]);
|
await db.run('UPDATE images SET thumb_filename = $1 WHERE id = $2', [thumbFilename, row.id]);
|
||||||
done++;
|
done++;
|
||||||
|
console.log(`[backfill-thumbs] ✅ id ${row.id}: ${thumbFilename}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(`[backfill-thumbs] ❌ id ${row.id}:`, e.message);
|
||||||
failed++;
|
failed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-9
@@ -525,6 +525,24 @@ app.use(checkInstallationMiddleware);
|
|||||||
// Middleware que pula autenticação para rotas públicas de imagens
|
// Middleware que pula autenticação para rotas públicas de imagens
|
||||||
const publicImageRoutes = express.Router();
|
const publicImageRoutes = express.Router();
|
||||||
|
|
||||||
|
// Tenta ler um arquivo do storage com fallback para o path legado ('Desconhecido').
|
||||||
|
// Uploads antigos foram salvos no Wasabi com patientName='Desconhecido' porque o handler
|
||||||
|
// de presigned-urls usava firstName/lastName (que o client nunca envia) em vez de name.
|
||||||
|
async function getImageBufferWithFallback(filename, clinicName, clientName, patientName) {
|
||||||
|
try {
|
||||||
|
const buf = await storage.getImageBuffer(filename, clinicName, clientName, patientName);
|
||||||
|
if (buf) return buf;
|
||||||
|
} catch (_) {}
|
||||||
|
// Fallback: path legado onde patientName era sempre 'Desconhecido'
|
||||||
|
if (patientName !== 'Desconhecido') {
|
||||||
|
try {
|
||||||
|
const buf = await storage.getImageBuffer(filename, clinicName, clientName, 'Desconhecido');
|
||||||
|
if (buf) return buf;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Serve os BYTES da thumbnail diretamente (image/jpeg), para uso direto em <img src>.
|
// 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,
|
// 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.
|
// serve imediatamente e faz backfill (upload pro Wasabi + atualiza banco) em background.
|
||||||
@@ -537,13 +555,12 @@ publicImageRoutes.get('/:id/thumbnail', async (req, res) => {
|
|||||||
if (!image) return res.status(404).json({ error: 'Imagem não encontrada' });
|
if (!image) return res.status(404).json({ error: 'Imagem não encontrada' });
|
||||||
|
|
||||||
// Thumb de um id nunca muda → cache agressivo no navegador (1 ano, imutável).
|
// Thumb de um id nunca muda → cache agressivo no navegador (1 ano, imutável).
|
||||||
// Repetir visita = 0 requisição ao servidor (served from disk cache).
|
|
||||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||||
|
|
||||||
// 1. Já existe thumbnail — serve direto (local ou Wasabi)
|
// 1. Já existe thumbnail — serve direto (local ou Wasabi, com fallback legado)
|
||||||
if (image.thumb_filename) {
|
if (image.thumb_filename) {
|
||||||
try {
|
try {
|
||||||
const buf = await storage.getImageBuffer(image.thumb_filename, image.clinic_name, image.client_name, image.patient_name);
|
const buf = await getImageBufferWithFallback(image.thumb_filename, image.clinic_name, image.client_name, image.patient_name);
|
||||||
if (buf) {
|
if (buf) {
|
||||||
res.setHeader('Content-Type', 'image/jpeg');
|
res.setHeader('Content-Type', 'image/jpeg');
|
||||||
return res.end(buf);
|
return res.end(buf);
|
||||||
@@ -554,10 +571,10 @@ publicImageRoutes.get('/:id/thumbnail', async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Sem thumbnail — puxa a original, gera a thumb na hora
|
// 2. Sem thumbnail — puxa a original (com fallback legado), gera a thumb na hora
|
||||||
const originalBuf = await storage.getImageBuffer(image.filename, image.clinic_name, image.client_name, image.patient_name);
|
const originalBuf = await getImageBufferWithFallback(image.filename, image.clinic_name, image.client_name, image.patient_name);
|
||||||
if (!originalBuf) {
|
if (!originalBuf) {
|
||||||
return res.status(404).json({ error: 'Imagem original não encontrada' });
|
return res.status(404).json({ error: 'Imagem original não encontrada no storage' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const thumbBuf = await imageProcessor.getThumbnail(originalBuf, 500, 500);
|
const thumbBuf = await imageProcessor.getThumbnail(originalBuf, 500, 500);
|
||||||
@@ -566,7 +583,7 @@ publicImageRoutes.get('/:id/thumbnail', async (req, res) => {
|
|||||||
res.setHeader('Content-Type', 'image/jpeg');
|
res.setHeader('Content-Type', 'image/jpeg');
|
||||||
res.end(thumbBuf);
|
res.end(thumbBuf);
|
||||||
|
|
||||||
// Backfill em background: salva a thumb no Wasabi e atualiza o banco
|
// Backfill em background: salva a thumb no Wasabi (path correto) e atualiza o banco
|
||||||
const thumbFilename = `thumb_${image.filename.replace(/\.[^.]+$/, '')}.jpg`;
|
const thumbFilename = `thumb_${image.filename.replace(/\.[^.]+$/, '')}.jpg`;
|
||||||
storage.saveImage(thumbFilename, thumbBuf, image.clinic_name, image.client_name, image.patient_name, false)
|
storage.saveImage(thumbFilename, thumbBuf, image.clinic_name, image.client_name, image.patient_name, false)
|
||||||
.then(() => db.run('UPDATE images SET thumb_filename = $1 WHERE id = $2', [thumbFilename, image.id]))
|
.then(() => db.run('UPDATE images SET thumb_filename = $1 WHERE id = $2', [thumbFilename, image.id]))
|
||||||
@@ -913,7 +930,7 @@ io.on('connection', (socket) => {
|
|||||||
const clientInfo = connectedClients.find(c => c.socketId === socket.id);
|
const clientInfo = connectedClients.find(c => c.socketId === socket.id);
|
||||||
const clientName = clientInfo ? clientInfo.name : 'PC não identificado';
|
const clientName = clientInfo ? clientInfo.name : 'PC não identificado';
|
||||||
const clinicName = socket.user?.clinicName || (clientInfo ? clientInfo.clinicName : 'Clínica não identificada');
|
const clinicName = socket.user?.clinicName || (clientInfo ? clientInfo.clinicName : 'Clínica não identificada');
|
||||||
const patientName = data.patientData ? `${data.patientData.firstName || ''} ${data.patientData.lastName || ''}`.trim() || 'Desconhecido' : 'Desconhecido';
|
const patientName = (data.patientData && data.patientData.name && data.patientData.name.trim()) ? data.patientData.name.trim() : 'Desconhecido';
|
||||||
|
|
||||||
// Salvar imagem original local e na nuvem
|
// Salvar imagem original local e na nuvem
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
@@ -1025,7 +1042,7 @@ io.on('connection', (socket) => {
|
|||||||
const clientInfo = connectedClients.find(c => c.socketId === socket.id);
|
const clientInfo = connectedClients.find(c => c.socketId === socket.id);
|
||||||
const clientName = clientInfo ? clientInfo.name : 'PC não identificado';
|
const clientName = clientInfo ? clientInfo.name : 'PC não identificado';
|
||||||
const clinicName = socket.user?.clinicName || (clientInfo ? clientInfo.clinicName : 'Clínica não identificada');
|
const clinicName = socket.user?.clinicName || (clientInfo ? clientInfo.clinicName : 'Clínica não identificada');
|
||||||
const patientName = data.patientData ? `${data.patientData.firstName || ''} ${data.patientData.lastName || ''}`.trim() || 'Desconhecido' : 'Desconhecido';
|
const patientName = (data.patientData && data.patientData.name && data.patientData.name.trim()) ? data.patientData.name.trim() : 'Desconhecido';
|
||||||
|
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const filename = `${timestamp}_${data.metadata.guid}.png`;
|
const filename = `${timestamp}_${data.metadata.guid}.png`;
|
||||||
|
|||||||
Reference in New Issue
Block a user