feat(storage): reorganiza Wasabi com estrutura clinic_name/pc_name/patient_name
continuous-integration/webhook Deploy concluído (VPS4)

- Adiciona coluna clinic_name em images (ALTER TABLE IF NOT EXISTS)
- Todas as funções de storage recebem clinicName como parâmetro: saveImage,
  getImageBuffer, getImageUrl, deleteImage, getDownloadPresignedUrl, getUploadPresignedUrl
- Chave Wasabi passa de pc_name/patient/arquivo → clinic_name/pc_name/patient/arquivo
- server.js: usa socket.user.clinicName (preenchido via machine_token auth do DB) como
  fonte primária do nome da clínica nos 3 handlers de upload e na rota thumbnail
- Todos os INSERTs em images passam a gravar clinic_name
- Corrige bug oculto: wipe de clínica agora apaga o prefixo correto no Wasabi
- Scripts utilitários atualizados: backfill-wasabi-thumbs, recreate-all-thumbs, migrate-to-wasabi
- client-monitor.js: envia clinicName no client-identify (fallback para clientes legados)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-05-31 01:56:18 +02:00
parent 651afb49bc
commit 21f33c88a4
8 changed files with 202 additions and 82 deletions
+36 -28
View File
@@ -531,7 +531,7 @@ const publicImageRoutes = express.Router();
publicImageRoutes.get('/:id/thumbnail', async (req, res) => {
try {
const image = await db.get(
'SELECT id, thumb_filename, filename, client_name, patient_name FROM images WHERE id = $1',
'SELECT id, thumb_filename, 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' });
@@ -543,7 +543,7 @@ publicImageRoutes.get('/:id/thumbnail', async (req, res) => {
// 1. Já existe thumbnail — serve direto (local ou Wasabi)
if (image.thumb_filename) {
try {
const buf = await storage.getImageBuffer(image.thumb_filename, image.client_name, image.patient_name);
const buf = await storage.getImageBuffer(image.thumb_filename, image.clinic_name, image.client_name, image.patient_name);
if (buf) {
res.setHeader('Content-Type', 'image/jpeg');
return res.end(buf);
@@ -555,7 +555,7 @@ publicImageRoutes.get('/:id/thumbnail', async (req, res) => {
}
// 2. Sem thumbnail — puxa a original, gera a thumb na hora
const originalBuf = await storage.getImageBuffer(image.filename, image.client_name, image.patient_name);
const originalBuf = await storage.getImageBuffer(image.filename, image.clinic_name, image.client_name, image.patient_name);
if (!originalBuf) {
return res.status(404).json({ error: 'Imagem original não encontrada' });
}
@@ -568,7 +568,7 @@ publicImageRoutes.get('/:id/thumbnail', async (req, res) => {
// 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)
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(() => console.log(`✅ [thumb-backfill] Thumb gerada/salva: ${thumbFilename} (id ${image.id})`))
.catch(err => console.error(`⚠️ [thumb-backfill] Falha id ${image.id}:`, err.message));
@@ -830,6 +830,7 @@ io.on('connection', (socket) => {
socketId: socket.id,
type: data.type || 'unknown',
name: clientName,
clinicName: socket.user?.clinicName || (data.clinicName || '').trim() || 'Clínica não identificada',
version: data.version || '1.0.0',
system: data.system || 'unknown',
clientId: clientId,
@@ -906,20 +907,21 @@ io.on('connection', (socket) => {
// Buscar nome do cliente se identificado
const clientInfo = connectedClients.find(c => c.socketId === socket.id);
const clientName = clientInfo ? clientInfo.name : 'Cliente 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 patientName = data.patientData ? `${data.patientData.firstName || ''} ${data.patientData.lastName || ''}`.trim() || 'Desconhecido' : 'Desconhecido';
// Salvar imagem original local e na nuvem
const timestamp = Date.now();
const filename = `${timestamp}_${data.metadata.guid}.png`;
await storage.saveImage(filename, imageBuffer, clientName, patientName, false);
await storage.saveImage(filename, imageBuffer, clinicName, clientName, patientName, false);
// Gerar e salvar miniatura (Thumbnail) — 500x500, qualidade 80%
let thumbFilename = null;
try {
const thumbBuffer = await imageProcessor.getThumbnail(imageBuffer, 500, 500);
thumbFilename = `thumb_${filename}`;
await storage.saveImage(thumbFilename, thumbBuffer, clientName, patientName, false);
await storage.saveImage(thumbFilename, thumbBuffer, clinicName, clientName, patientName, false);
} catch (thumbErr) {
console.error('Erro ao gerar thumbnail:', thumbErr);
// Continua mesmo se falhar o thumbnail, usando fallback no front
@@ -936,20 +938,22 @@ io.on('connection', (socket) => {
// Salvar no banco de dados
const result = await db.run(
`INSERT INTO images (
image_guid,
image_guid,
patient_name,
clinic_name,
client_name,
original_filename,
filename,
original_filename,
filename,
enabled,
doctor,
remark,
thumb_filename,
created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`,
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id`,
[
data.metadata.guid,
data.patientData.name || 'Paciente não identificado',
clinicName,
clientName,
data.metadata.fileName,
filename,
@@ -960,7 +964,7 @@ io.on('connection', (socket) => {
createdAt
]
);
console.log('✅ Imagem salva no banco. ID:', result.lastID);
console.log('✅ Arquivo submetido ao storage (Wasabi/Local):', filename);
@@ -1015,15 +1019,16 @@ io.on('connection', (socket) => {
}
const clientInfo = connectedClients.find(c => c.socketId === socket.id);
const clientName = clientInfo ? clientInfo.name : 'Cliente 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 patientName = data.patientData ? `${data.patientData.firstName || ''} ${data.patientData.lastName || ''}`.trim() || 'Desconhecido' : 'Desconhecido';
const timestamp = Date.now();
const filename = `${timestamp}_${data.metadata.guid}.png`;
const thumbFilename = `${timestamp}_${data.metadata.guid}_thumb.jpg`;
const originalUrl = await storage.getUploadPresignedUrl(filename, clientName, patientName, 'image/png');
const thumbnailUrl = await storage.getUploadPresignedUrl(thumbFilename, clientName, patientName, 'image/jpeg');
const originalUrl = await storage.getUploadPresignedUrl(filename, clinicName, clientName, patientName, 'image/png');
const thumbnailUrl = await storage.getUploadPresignedUrl(thumbFilename, clinicName, clientName, patientName, 'image/jpeg');
if (!originalUrl || !thumbnailUrl) {
if (typeof callback === 'function') callback({ success: false, error: 'Falha ao gerar URLs no S3.' });
@@ -1051,33 +1056,36 @@ io.on('connection', (socket) => {
console.log('📥 [image-upload-direct] Notificação de upload recebida de:', socket.id);
const clientInfo = connectedClients.find(c => c.socketId === socket.id);
const clientName = clientInfo ? clientInfo.name : 'Cliente 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');
// Determinar data de criação
const formatLocalDateTime = (date) => {
const pad = (num) => String(num).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const createdAt = (data.patientData && data.patientData.photographTime) || formatLocalDateTime(new Date());
// Salvar no banco de dados com a referência para a thumbnail
const result = await db.run(
`INSERT INTO images (
image_guid,
image_guid,
patient_name,
clinic_name,
client_name,
original_filename,
filename,
original_filename,
filename,
enabled,
doctor,
remark,
thumb_filename,
created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`,
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id`,
[
data.metadata.guid,
data.patientData.name || 'Paciente não identificado',
clinicName,
clientName,
data.metadata.fileName,
data.filename,
@@ -1253,7 +1261,7 @@ async function start() {
try {
const { runThumbBackfill } = require('./routes/images');
const missing = await db.all(
`SELECT id, filename, client_name, patient_name FROM images
`SELECT id, filename, clinic_name, client_name, patient_name FROM images
WHERE thumb_filename IS NULL AND enabled = true ORDER BY created_at DESC`
);
if (missing.length > 0) {