feat(client): envia clinicName no identify e migra upload para Presigned URLs
- client-monitor.js: adiciona clinicName no client-identify (fallback para auth legada) - Direct Upload via Presigned URLs (bypass VPS) como caminho principal - Fallback automático para envio via VPS se Wasabi não disponível - Thumbnail gerado localmente via IPC com index.js (remove dependência sharp) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+119
-55
@@ -5,22 +5,27 @@ const path = require('path');
|
||||
const readline = require('readline');
|
||||
const http = require('http');
|
||||
const { exec } = require('child_process');
|
||||
const sharp = require('sharp');
|
||||
|
||||
// Função para comprimir imagem PNG/JPEG de alta resolução para JPEG otimizado
|
||||
async function comprimirImagem(buffer) {
|
||||
return await sharp(buffer)
|
||||
.resize({
|
||||
width: 1600,
|
||||
withoutEnlargement: true,
|
||||
fit: 'inside'
|
||||
})
|
||||
.jpeg({
|
||||
quality: 75,
|
||||
mozjpeg: true,
|
||||
progressive: true
|
||||
})
|
||||
.toBuffer();
|
||||
// Função para gerar o thumbnail usando a CPU local do cliente (via IPC com o index.js do Electron)
|
||||
async function gerarThumbnail(buffer) {
|
||||
return new Promise((resolve) => {
|
||||
if (!process.send) return resolve(null); // Fallback se não for fork
|
||||
|
||||
const id = Date.now().toString() + Math.random().toString();
|
||||
const timeout = setTimeout(() => resolve(null), 5000);
|
||||
|
||||
const listener = (message) => {
|
||||
if (message && message.event === 'resize-result' && message.id === id) {
|
||||
clearTimeout(timeout);
|
||||
process.removeListener('message', listener);
|
||||
if (message.success) resolve(Buffer.from(message.thumbBase64, 'base64'));
|
||||
else resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
process.on('message', listener);
|
||||
process.send({ event: 'resize-image', id, base64: buffer.toString('base64') });
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve dynamic folder in AppData or default path passed by Electron
|
||||
@@ -1084,6 +1089,7 @@ function identifyClient() {
|
||||
socket.emit('client-identify', {
|
||||
type: 'windows', // MANDATÓRIO: Identifica o notebook na Área de Sincronização
|
||||
name: config.clientName || 'Notebook Consultório Principal', // Nome amigável que aparecerá no painel
|
||||
clinicName: config.clinicName || '', // Nome da clínica (preenchido pelo servidor via machine_token auth)
|
||||
version: '1.1.0',
|
||||
system: 'windows',
|
||||
path: config.monitorPath,
|
||||
@@ -1291,13 +1297,19 @@ function sendImageToServer(filePath, isBacklog, bypassCache = false) {
|
||||
let fileBuffer = fs.readFileSync(filePath);
|
||||
const originalSize = fileBuffer.length;
|
||||
const isImage = /\.(png|jpg|jpeg)$/i.test(fileName);
|
||||
let thumbBuffer = null;
|
||||
|
||||
if (isImage) {
|
||||
try {
|
||||
fileBuffer = await comprimirImagem(fileBuffer);
|
||||
addLog(`⚡ Imagem comprimida com sucesso: ${fileName} (${Math.round(originalSize / 1024)} KB -> ${Math.round(fileBuffer.length / 1024)} KB)`);
|
||||
} catch (compressErr) {
|
||||
addLog(`⚠️ Falha na compressão da imagem ${fileName}, enviando original: ${compressErr.message}`);
|
||||
addLog(`⚡ Gerando miniatura localmente para: ${fileName}...`);
|
||||
thumbBuffer = await gerarThumbnail(fileBuffer);
|
||||
if (thumbBuffer) {
|
||||
addLog(`✅ Miniatura gerada com sucesso (${Math.round(thumbBuffer.length / 1024)} KB)`);
|
||||
} else {
|
||||
addLog(`⚠️ Não foi possível gerar miniatura, o servidor precisará usar o original.`);
|
||||
}
|
||||
} catch (e) {
|
||||
addLog(`⚠️ Erro ao gerar miniatura: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1312,58 +1324,110 @@ function sendImageToServer(filePath, isBacklog, bypassCache = false) {
|
||||
doctor = localInfo.doctor;
|
||||
remark = localInfo.remark;
|
||||
photographTime = localInfo.photographTime;
|
||||
addLog(`🔍 Dados do paciente encontrados localmente: "${patientName}", Dentista: "${doctor}", Obs: "${remark}", Data: "${photographTime}"`);
|
||||
addLog(`🔍 Dados do paciente: "${patientName}", Dentista: "${doctor}"`);
|
||||
} else {
|
||||
addLog(`🔍 Nenhum registro no banco local para o arquivo: ${fileName}`);
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
const fileDate = stat.mtime || stat.birthtime || new Date();
|
||||
const pad = (num) => String(num).padStart(2, '0');
|
||||
photographTime = `${fileDate.getFullYear()}-${pad(fileDate.getMonth() + 1)}-${pad(fileDate.getDate())} ${pad(fileDate.getHours())}:${pad(fileDate.getMinutes())}:${pad(fileDate.getSeconds())}`;
|
||||
addLog(`ℹ️ Usando data de modificação do arquivo como fallback: "${photographTime}"`);
|
||||
} catch (e) {
|
||||
addLog(`⚠️ Erro ao ler metadados do arquivo: ${e.message}`);
|
||||
addLog(`⚠️ Erro ao ler metadados: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
addLog(`📤 Enviando: ${fileName} (${Math.round(fileBuffer.length / 1024)} KB) ${isBacklog ? '[Backlog]' : '[Novo]'}`);
|
||||
const metadata = {
|
||||
fileName: fileName,
|
||||
guid: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
fileSize: fileBuffer.length
|
||||
};
|
||||
|
||||
const patientData = {
|
||||
name: patientName,
|
||||
doctor: doctor,
|
||||
remark: remark,
|
||||
photographTime: photographTime
|
||||
};
|
||||
|
||||
// Definir timeout de 45 segundos para segurança
|
||||
let didTimeout = false;
|
||||
const timeoutId = setTimeout(() => {
|
||||
didTimeout = true;
|
||||
reject(new Error(`Timeout de 45 segundos aguardando resposta do servidor para: ${fileName}`));
|
||||
}, 45000);
|
||||
addLog(`📤 Solicitando URLs temporárias para envio de: ${fileName} (${Math.round(fileBuffer.length / 1024)} KB)`);
|
||||
|
||||
socket.emit('send-image', {
|
||||
imageBase64: fileBuffer.toString('base64'),
|
||||
isBacklog: isBacklog,
|
||||
metadata: {
|
||||
fileName: fileName,
|
||||
guid: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
fileSize: fileBuffer.length
|
||||
},
|
||||
patientData: {
|
||||
name: patientName,
|
||||
doctor: doctor,
|
||||
remark: remark,
|
||||
photographTime: photographTime
|
||||
}
|
||||
}, (response) => {
|
||||
if (didTimeout) return;
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (response && response.success) {
|
||||
addLog(`✅ Confirmação do servidor recebida: ${response.message || 'sucesso'}`);
|
||||
if (response.originalFilename) {
|
||||
uploadedCache.add(response.originalFilename);
|
||||
saveUploadCache();
|
||||
// Pedir Presigned URLs
|
||||
socket.emit('request-presigned-urls', { metadata, patientData }, async (urlResponse) => {
|
||||
if (urlResponse && urlResponse.success) {
|
||||
try {
|
||||
addLog(`🌐 Fazendo Direct Upload pro Wasabi (Bypass VPS)...`);
|
||||
// Fazer PUT da original
|
||||
await fetch(urlResponse.originalUrl, {
|
||||
method: 'PUT',
|
||||
body: fileBuffer,
|
||||
headers: { 'Content-Type': 'image/png' }
|
||||
});
|
||||
|
||||
// Fazer PUT do thumb se existir
|
||||
if (thumbBuffer && urlResponse.thumbnailUrl) {
|
||||
await fetch(urlResponse.thumbnailUrl, {
|
||||
method: 'PUT',
|
||||
body: thumbBuffer,
|
||||
headers: { 'Content-Type': 'image/jpeg' }
|
||||
});
|
||||
}
|
||||
|
||||
addLog(`✅ Upload direto concluído! Notificando servidor...`);
|
||||
|
||||
// Notificar o servidor que o upload terminou
|
||||
socket.emit('image-upload-direct', {
|
||||
metadata,
|
||||
patientData,
|
||||
filename: urlResponse.filename,
|
||||
thumbFilename: thumbBuffer ? urlResponse.thumbFilename : null
|
||||
}, (response) => {
|
||||
if (response && response.success) {
|
||||
uploadedCache.add(response.originalFilename);
|
||||
saveUploadCache();
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(response ? response.error : 'Erro na notificação de direct-upload'));
|
||||
}
|
||||
});
|
||||
|
||||
} catch (uploadErr) {
|
||||
addLog(`❌ Falha no Direct Upload: ${uploadErr.message}. Tentando via VPS...`);
|
||||
fallbackUpload();
|
||||
}
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(response ? response.error : 'Erro desconhecido do servidor'));
|
||||
addLog(`ℹ️ Wasabi não disponível no servidor. Usando envio padrão via VPS.`);
|
||||
fallbackUpload();
|
||||
}
|
||||
});
|
||||
|
||||
function fallbackUpload() {
|
||||
let didTimeout = false;
|
||||
const timeoutId = setTimeout(() => {
|
||||
didTimeout = true;
|
||||
reject(new Error(`Timeout de 45 segundos aguardando resposta do servidor para: ${fileName}`));
|
||||
}, 45000);
|
||||
|
||||
socket.emit('send-image', {
|
||||
imageBase64: fileBuffer.toString('base64'),
|
||||
isBacklog: isBacklog,
|
||||
metadata: metadata,
|
||||
patientData: patientData
|
||||
}, (response) => {
|
||||
if (didTimeout) return;
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (response && response.success) {
|
||||
addLog(`✅ Confirmação do servidor recebida: ${response.message || 'sucesso'}`);
|
||||
if (response.originalFilename) {
|
||||
uploadedCache.add(response.originalFilename);
|
||||
saveUploadCache();
|
||||
}
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(response ? response.error : 'Erro desconhecido do servidor'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
addLog(`❌ Erro ao ler/enviar arquivo: ${error.message}`);
|
||||
|
||||
Reference in New Issue
Block a user