e716e0e131
continuous-integration/webhook Deploy concluído (VPS4)
Bug fix: - server.js e routes/images.js: 'enabled = true' → 'enabled = 1' (PostgreSQL rejeita smallint = boolean — causava 'operator does not exist') Plan B — imagens ausentes no Wasabi: - server.js: pendingReupload (Map clientName → Set<original_filename>) quando getImageBufferWithFallback retorna null no endpoint /thumbnail, scheduleReupload() registra o original_filename e emite 'reupload-files' imediatamente se o cliente estiver online, ou na próxima conexão. - server.js: client-identify agora drena a fila pendente do cliente ao reconectar, antes de emitir clients-list-updated. - original_filename adicionado ao SELECT do endpoint /thumbnail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
621 lines
23 KiB
JavaScript
621 lines
23 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const db = require('../database');
|
|
const imageProcessor = require('../image-processor');
|
|
const path = require('path');
|
|
const fs = require('fs').promises;
|
|
const storage = require('../storage');
|
|
|
|
// ================================================================
|
|
// GET /api/images/unique-clients - Buscar lista leve de clínicas
|
|
// ================================================================
|
|
router.get('/unique-clients', async (req, res) => {
|
|
try {
|
|
const clients = await db.all('SELECT DISTINCT client_name FROM images WHERE client_name IS NOT NULL');
|
|
res.json(clients.map(c => c.client_name));
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// POST /api/images/upload - Envio Manual via Web
|
|
// ================================================================
|
|
router.post('/upload', async (req, res) => {
|
|
try {
|
|
const { imageBase64, patientData } = req.body;
|
|
|
|
if (!imageBase64 || !patientData || !patientData.name) {
|
|
return res.status(400).json({ error: 'Dados incompletos: imageBase64 e patientData.name são obrigatórios.' });
|
|
}
|
|
|
|
const base64Data = imageBase64.replace(/^data:image\/\w+;base64,/, "");
|
|
const imageBuffer = Buffer.from(base64Data, 'base64');
|
|
|
|
await imageProcessor.validateImage(imageBuffer);
|
|
|
|
const clinicName = 'Upload Manual';
|
|
const clientName = 'Upload Manual';
|
|
const patientName = patientData.name;
|
|
const timestamp = Date.now();
|
|
const guid = Math.random().toString(36).substring(2, 15);
|
|
const filename = `${timestamp}_${guid}.png`;
|
|
|
|
await storage.saveImage(filename, imageBuffer, clinicName, clientName, patientName, false);
|
|
|
|
let thumbFilename = null;
|
|
try {
|
|
const thumbBuffer = await imageProcessor.getThumbnail(imageBuffer, 500, 500);
|
|
thumbFilename = `thumb_${filename}`;
|
|
await storage.saveImage(thumbFilename, thumbBuffer, clinicName, clientName, patientName, false);
|
|
} catch (e) {
|
|
console.error('Erro ao gerar thumbnail manual:', e);
|
|
}
|
|
|
|
const doctor = patientData.doctor || '';
|
|
const remark = patientData.remark || '';
|
|
|
|
const result = await db.run(
|
|
`INSERT INTO images (
|
|
image_guid, patient_name, clinic_name, client_name, original_filename, filename, thumb_filename, enabled, doctor, remark
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, 1, $8, $9)`,
|
|
[guid, patientName, clinicName, clientName, 'manual_upload.png', filename, thumbFilename, doctor, remark]
|
|
);
|
|
|
|
// Notificar clientes socket para atualizar galeria automaticamente
|
|
if (req.app.get('io') || global.io) {
|
|
const io = req.app.get('io') || global.io;
|
|
if (io) io.emit('new-image', { filename: filename, thumb: thumbFilename, patientName: patientName });
|
|
}
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Erro no upload manual:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// GET /api/images - Listar imagens
|
|
// ================================================================
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const showDisabled = req.query.disabled === 'true';
|
|
const search = req.query.search || '';
|
|
const clientName = req.query.client || '';
|
|
|
|
let query = `SELECT * FROM images WHERE enabled = $1`;
|
|
let params = [showDisabled ? 0 : 1];
|
|
let paramIndex = 2;
|
|
|
|
if (clientName) {
|
|
query += ` AND client_name = $${paramIndex++}`;
|
|
params.push(clientName);
|
|
}
|
|
|
|
if (search) {
|
|
query += ` AND (patient_name LIKE $${paramIndex} OR image_guid LIKE $${paramIndex + 1} OR filename LIKE $${paramIndex + 2})`;
|
|
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
|
paramIndex += 3;
|
|
}
|
|
|
|
query += ` ORDER BY created_at DESC LIMIT 100`;
|
|
|
|
const images = await db.all(query, params);
|
|
res.json(images);
|
|
} catch (error) {
|
|
console.error('Erro ao listar imagens:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// GET /api/images/patients - Listar pacientes agrupados
|
|
// ================================================================
|
|
router.get('/patients', async (req, res) => {
|
|
try {
|
|
const showDisabled = req.query.disabled === 'true';
|
|
const search = req.query.search || '';
|
|
const clientName = req.query.client || '';
|
|
const enabledVal = showDisabled ? 0 : 1;
|
|
|
|
let paramIndex = 1;
|
|
let where = `WHERE enabled = $${paramIndex++}`;
|
|
let params = [enabledVal];
|
|
|
|
if (clientName) {
|
|
where += ` AND client_name = $${paramIndex}`;
|
|
params.push(clientName);
|
|
paramIndex++;
|
|
}
|
|
|
|
// Pagination
|
|
const page = parseInt(req.query.page) || 1;
|
|
const limit = parseInt(req.query.limit) || 50;
|
|
const offset = (page - 1) * limit;
|
|
|
|
if (search) {
|
|
where += ` AND (patient_name LIKE $${paramIndex} OR image_guid LIKE $${paramIndex + 1})`;
|
|
params.push(`%${search}%`, `%${search}%`);
|
|
paramIndex += 2;
|
|
}
|
|
|
|
// Group by patient_name — pick thumbnail from most recent image
|
|
const rows = await db.all(
|
|
`SELECT
|
|
patient_name,
|
|
MAX(doctor) AS doctor,
|
|
MAX(remark) AS remark,
|
|
MAX(client_name) AS client_name,
|
|
COUNT(*) AS image_count,
|
|
MAX(created_at) AS last_date,
|
|
(SELECT id FROM images i2
|
|
WHERE i2.patient_name = images.patient_name AND i2.enabled = ${enabledVal}
|
|
ORDER BY i2.created_at DESC LIMIT 1) AS latest_image_id,
|
|
(SELECT COALESCE(thumb_filename, filename) FROM images i2
|
|
WHERE i2.patient_name = images.patient_name AND i2.enabled = ${enabledVal}
|
|
ORDER BY i2.created_at DESC LIMIT 1) AS thumb_filename
|
|
FROM images
|
|
${where}
|
|
GROUP BY patient_name
|
|
ORDER BY MAX(created_at) DESC
|
|
LIMIT ${limit} OFFSET ${offset}`,
|
|
params
|
|
);
|
|
|
|
for (const row of rows) {
|
|
row.thumb_url = row.latest_image_id ? `/api/images/${row.latest_image_id}/thumbnail` : '';
|
|
}
|
|
|
|
res.json(rows);
|
|
} catch (error) {
|
|
console.error('Erro ao listar pacientes:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
|
|
// ================================================================
|
|
// GET /api/images/by-patient - Imagens de um paciente
|
|
// ================================================================
|
|
router.get('/by-patient', async (req, res) => {
|
|
try {
|
|
const patientName = req.query.name || '';
|
|
const showDisabled = req.query.disabled === 'true';
|
|
|
|
if (!patientName) {
|
|
return res.status(400).json({ error: 'Parâmetro name obrigatório' });
|
|
}
|
|
|
|
const images = await db.all(
|
|
`SELECT * FROM images
|
|
WHERE patient_name = $1 AND enabled = $2
|
|
ORDER BY created_at DESC`,
|
|
[patientName, showDisabled ? 0 : 1]
|
|
);
|
|
|
|
// Adicionar lazy-load URLs (não fazer requisições async aqui)
|
|
for (const image of images) {
|
|
image.file_url = `/api/images/${image.id}/file-url`;
|
|
image.thumb_url = `/api/images/${image.id}/thumbnail`;
|
|
}
|
|
|
|
res.json(images);
|
|
} catch (error) {
|
|
console.error('Erro ao buscar imagens do paciente:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// PUT /api/images/patients/:name - Editar dados de um paciente
|
|
// Atualiza patient_name, doctor e remark em todas as imagens do paciente.
|
|
// ================================================================
|
|
router.put('/patients/:name', async (req, res) => {
|
|
try {
|
|
const oldName = decodeURIComponent(req.params.name);
|
|
const { newName, doctor, remark } = req.body;
|
|
|
|
if (!oldName) return res.status(400).json({ error: 'Nome do paciente obrigatório.' });
|
|
|
|
const updates = [];
|
|
const params = [];
|
|
let i = 1;
|
|
|
|
if (newName !== undefined && newName.trim() !== '') {
|
|
updates.push(`patient_name = $${i++}`);
|
|
params.push(newName.trim());
|
|
}
|
|
if (doctor !== undefined) {
|
|
updates.push(`doctor = $${i++}`);
|
|
params.push(doctor);
|
|
}
|
|
if (remark !== undefined) {
|
|
updates.push(`remark = $${i++}`);
|
|
params.push(remark);
|
|
}
|
|
|
|
if (updates.length === 0) return res.status(400).json({ error: 'Nenhum campo para atualizar.' });
|
|
|
|
params.push(oldName);
|
|
const result = await db.run(
|
|
`UPDATE images SET ${updates.join(', ')} WHERE patient_name = $${i}`,
|
|
params
|
|
);
|
|
|
|
res.json({ success: true, updated: result.changes });
|
|
} catch (error) {
|
|
console.error('Erro ao editar paciente:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// DELETE /api/images/by-patient - Excluir todas imagens de um paciente
|
|
// ================================================================
|
|
router.delete('/by-patient', async (req, res) => {
|
|
try {
|
|
const patientName = req.query.name;
|
|
if (!patientName) return res.status(400).json({ error: 'Parâmetro name obrigatório' });
|
|
|
|
const images = await db.all('SELECT * FROM images WHERE patient_name = $1', [patientName]);
|
|
|
|
for (const image of images) {
|
|
await storage.deleteImage(image.filename, image.clinic_name, image.client_name, image.patient_name);
|
|
}
|
|
|
|
await db.run('DELETE FROM images WHERE patient_name = $1', [patientName]);
|
|
res.json({ success: true, count: images.length });
|
|
} catch (error) {
|
|
console.error('Erro ao excluir imagens do paciente:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// GET /api/images/:id - Buscar imagem por ID
|
|
// ================================================================
|
|
router.get('/:id', async (req, res) => {
|
|
try {
|
|
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
|
if (!image) {
|
|
return res.status(404).json({ error: 'Imagem não encontrada' });
|
|
}
|
|
res.json(image);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// GET /api/images/stats - Estatísticas
|
|
// ================================================================
|
|
router.get('/stats', async (req, res) => {
|
|
try {
|
|
const total = await db.get('SELECT COUNT(*) as count FROM images');
|
|
const enabled = await db.get('SELECT COUNT(*) as count FROM images WHERE enabled = 1');
|
|
const withTooth = await db.get('SELECT COUNT(*) as count FROM images WHERE tooth_number IS NOT NULL');
|
|
|
|
res.json({
|
|
total: total.count,
|
|
enabled: enabled.count,
|
|
disabled: total.count - enabled.count,
|
|
withToothInfo: withTooth.count,
|
|
withoutToothInfo: total.count - withTooth.count
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// GET /api/images/:id/transformations - Gerar transformações
|
|
// ================================================================
|
|
router.get('/:id/transformations', async (req, res) => {
|
|
try {
|
|
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
|
if (!image) {
|
|
return res.status(404).json({ error: 'Imagem não encontrada' });
|
|
}
|
|
|
|
// Buscar a imagem original se esta é uma transformação
|
|
const originalId = image.original_image_id || image.id;
|
|
const originalImage = await db.get('SELECT * FROM images WHERE id = $1', [originalId]);
|
|
|
|
let originalBuffer;
|
|
try {
|
|
originalBuffer = await storage.getImageBuffer(originalImage.filename, originalImage.clinic_name, originalImage.client_name, originalImage.patient_name, false);
|
|
} catch (e) {
|
|
originalBuffer = await storage.getImageBuffer(originalImage.filename, originalImage.clinic_name, originalImage.client_name, originalImage.patient_name, true);
|
|
}
|
|
|
|
const transformations = [
|
|
{ name: 'Original', rotation: 0, flipH: false, flipV: false },
|
|
{ name: 'Flip Horizontal', rotation: 0, flipH: true, flipV: false },
|
|
{ name: 'Flip Vertical', rotation: 0, flipH: false, flipV: true },
|
|
{ name: 'Rotação +90°', rotation: 90, flipH: false, flipV: false },
|
|
{ name: 'Rotação -90°', rotation: -90, flipH: false, flipV: false }
|
|
];
|
|
|
|
const results = [];
|
|
for (const t of transformations) {
|
|
try {
|
|
let processed = await imageProcessor.rotate(originalBuffer, t.rotation);
|
|
processed = await imageProcessor.flip(processed, t.flipH, t.flipV);
|
|
const base64 = `data:image/png;base64,${processed.toString('base64')}`;
|
|
|
|
results.push({
|
|
...t,
|
|
base64,
|
|
size: processed.length
|
|
});
|
|
} catch (error) {
|
|
console.error(`Erro ao processar transformação ${t.name}:`, error);
|
|
}
|
|
}
|
|
|
|
res.json(results);
|
|
} catch (error) {
|
|
console.error('Erro ao gerar transformações:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// POST /api/images/:id/transform - Salvar transformação
|
|
// ================================================================
|
|
router.post('/:id/transform', async (req, res) => {
|
|
try {
|
|
const { rotation, flipH, flipV, remark } = req.body;
|
|
const originalImage = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
|
|
|
if (!originalImage) {
|
|
return res.status(404).json({ error: 'Imagem não encontrada' });
|
|
}
|
|
|
|
// Buscar a imagem original real se esta é uma transformação
|
|
const realOriginalId = originalImage.original_image_id || originalImage.id;
|
|
const realOriginal = await db.get('SELECT * FROM images WHERE id = $1', [realOriginalId]);
|
|
|
|
// Processar a imagem
|
|
let originalBuffer;
|
|
try {
|
|
originalBuffer = await storage.getImageBuffer(realOriginal.filename, realOriginal.clinic_name, realOriginal.client_name, realOriginal.patient_name, false);
|
|
} catch (e) {
|
|
originalBuffer = await storage.getImageBuffer(realOriginal.filename, realOriginal.clinic_name, realOriginal.client_name, realOriginal.patient_name, true);
|
|
}
|
|
|
|
let processed = await imageProcessor.rotate(originalBuffer, rotation);
|
|
processed = await imageProcessor.flip(processed, flipH, flipV);
|
|
|
|
// Salvar nova versão em processed (local e nuvem)
|
|
const timestamp = Date.now();
|
|
const processedFilename = `${realOriginal.image_guid}_${timestamp}.png`;
|
|
await storage.saveImage(processedFilename, processed, realOriginal.clinic_name, realOriginal.client_name, realOriginal.patient_name, true);
|
|
|
|
// Desabilitar a imagem active atual para que não fique duplicada na interface
|
|
await db.run('UPDATE images SET enabled = 0 WHERE id = $1', [req.params.id]);
|
|
|
|
const newRemark = remark !== undefined ? remark : originalImage.remark;
|
|
|
|
// Inserir a nova imagem como a versão ativa
|
|
const result = await db.run(
|
|
`INSERT INTO images (
|
|
image_guid, patient_name, clinic_name, client_name, original_filename, filename,
|
|
enabled, rotation, flip_horizontal, flip_vertical, original_image_id, created_at, remark, doctor
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`,
|
|
[
|
|
`${realOriginal.image_guid}_${timestamp}`,
|
|
realOriginal.patient_name,
|
|
realOriginal.clinic_name,
|
|
realOriginal.client_name,
|
|
realOriginal.filename,
|
|
processedFilename,
|
|
1, // Nova imagem fica ativa
|
|
rotation,
|
|
flipH ? 1 : 0,
|
|
flipV ? 1 : 0,
|
|
realOriginalId,
|
|
originalImage.created_at, // Manter a data de criação original
|
|
newRemark,
|
|
realOriginal.doctor
|
|
]
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
imageId: result.lastID,
|
|
message: 'Orientação atualizada com sucesso!'
|
|
});
|
|
} catch (error) {
|
|
console.error('Erro ao salvar transformação:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// PUT /api/images/:id/patient-info - Atualizar dados do paciente
|
|
// ================================================================
|
|
router.put('/:id/patient-info', async (req, res) => {
|
|
try {
|
|
const { patientNameResumed, toothNumber, toothSide } = req.body;
|
|
|
|
await db.run(
|
|
`UPDATE images SET
|
|
patient_name_resumed = $1,
|
|
tooth_number = $2,
|
|
tooth_side = $3,
|
|
enabled = 1
|
|
WHERE id = $4`,
|
|
[patientNameResumed, toothNumber, toothSide, req.params.id]
|
|
);
|
|
|
|
res.json({ success: true, message: 'Dados do paciente atualizados!' });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// GET /api/images/:id/download - Download da imagem
|
|
// ================================================================
|
|
router.get('/:id/download', async (req, res) => {
|
|
try {
|
|
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
|
if (!image) {
|
|
return res.status(404).json({ error: 'Imagem não encontrada' });
|
|
}
|
|
|
|
// Nome: nome-resumido_numero_lado.png
|
|
if (image.patient_name_resumed && image.tooth_number && image.tooth_side) {
|
|
const filename = `${image.patient_name_resumed}_${image.tooth_number}_${image.tooth_side}.png`;
|
|
|
|
const filePath = path.join(__dirname, '../processed', image.filename);
|
|
|
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
res.download(filePath, filename);
|
|
} else {
|
|
res.status(400).json({ error: 'Dados do paciente incompletos' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro no download:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// DELETE /api/images/:id - Apagar versão da imagem
|
|
// ================================================================
|
|
router.delete('/:id', async (req, res) => {
|
|
try {
|
|
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
|
if (!image) {
|
|
return res.status(404).json({ error: 'Imagem não encontrada' });
|
|
}
|
|
|
|
// NUNCA apagar a original
|
|
if (!image.original_image_id) {
|
|
return res.status(400).json({
|
|
error: 'Não é possível apagar a imagem original',
|
|
code: 'ORIGINAL_NOT_DELETABLE'
|
|
});
|
|
}
|
|
|
|
// Apagar arquivo local e do S3
|
|
await storage.deleteImage(image.filename, image.clinic_name, image.client_name, image.patient_name);
|
|
|
|
// Apagar do banco
|
|
await db.run('DELETE FROM images WHERE id = $1', [req.params.id]);
|
|
|
|
res.json({ success: true, message: 'Imagem apagada com sucesso' });
|
|
} catch (error) {
|
|
console.error('Erro ao apagar imagem:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// GET /api/images/:id/file-url — Retorna a URL de visualização
|
|
// full-res de uma imagem (para o painel Original do modal)
|
|
// ================================================================
|
|
router.get('/:id/file-url', async (req, res) => {
|
|
try {
|
|
const image = await db.get('SELECT filename, clinic_name, client_name, patient_name FROM images WHERE id = $1', [req.params.id]);
|
|
if (!image) return res.status(404).json({ error: 'Imagem não encontrada' });
|
|
|
|
let url;
|
|
if (image.filename.startsWith('http')) {
|
|
url = image.filename;
|
|
} else {
|
|
// Se o arquivo está no Wasabi, reconstroem a URL
|
|
try {
|
|
const wasabiUrl = await storage.getImageUrl(image.filename, image.clinic_name, image.client_name, image.patient_name);
|
|
url = wasabiUrl || `/uploads/${image.filename}`;
|
|
} catch (e) {
|
|
console.error('Erro ao gerar URL Wasabi:', e);
|
|
url = `/uploads/${image.filename}`;
|
|
}
|
|
}
|
|
res.json({ url });
|
|
} catch (e) {
|
|
res.status(500).json({ error: e.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// PUT /api/images/:id/toggle-enabled - Habilitar/Desabilitar
|
|
// ================================================================
|
|
router.put('/:id/toggle-enabled', async (req, res) => {
|
|
try {
|
|
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
|
if (!image) {
|
|
return res.status(404).json({ error: 'Imagem não encontrada' });
|
|
}
|
|
|
|
const newEnabled = image.enabled ? 0 : 1;
|
|
await db.run('UPDATE images SET enabled = $1 WHERE id = $2', [newEnabled, req.params.id]);
|
|
|
|
res.json({ success: true, enabled: newEnabled === 1 });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// POST /api/images/backfill-thumbs — Gera thumbnails para imagens
|
|
// que ainda não têm thumb_filename. Roda em background; retorna
|
|
// imediatamente com o total de imagens na fila.
|
|
// ================================================================
|
|
router.post('/backfill-thumbs', async (req, res) => {
|
|
try {
|
|
const rows = await db.all(
|
|
`SELECT id, filename, clinic_name, client_name, patient_name
|
|
FROM images
|
|
WHERE thumb_filename IS NULL AND enabled = 1
|
|
ORDER BY created_at DESC`
|
|
);
|
|
res.json({ queued: rows.length, message: 'Backfill iniciado em background' });
|
|
// Processa sem bloquear a resposta
|
|
setImmediate(() => runThumbBackfill(rows));
|
|
} catch (e) {
|
|
res.status(500).json({ error: e.message });
|
|
}
|
|
});
|
|
|
|
async function runThumbBackfill(rows) {
|
|
let done = 0, failed = 0;
|
|
for (const row of rows) {
|
|
try {
|
|
// Busca a original via storage (disco local → Wasabi com path correto → fallback 'Desconhecido')
|
|
let imgBuffer = null;
|
|
try {
|
|
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; }
|
|
|
|
const thumbBuffer = await imageProcessor.getThumbnail(imgBuffer);
|
|
const thumbFilename = `thumb_${row.filename.replace(/\.[^.]+$/, '')}.jpg`;
|
|
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]);
|
|
done++;
|
|
console.log(`[backfill-thumbs] ✅ id ${row.id}: ${thumbFilename}`);
|
|
} catch (e) {
|
|
console.error(`[backfill-thumbs] ❌ id ${row.id}:`, e.message);
|
|
failed++;
|
|
}
|
|
}
|
|
console.log(`[backfill-thumbs] concluído: ${done} geradas, ${failed} falhas de ${rows.length} total`);
|
|
}
|
|
|
|
// Exporta a função para ser chamada no boot do servidor
|
|
module.exports = router;
|
|
module.exports.runThumbBackfill = runThumbBackfill;
|