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
+3 -3
View File
@@ -19,7 +19,7 @@ async function run() {
}
const rows = await db.all(`
SELECT id, filename, client_name, patient_name
SELECT id, filename, clinic_name, client_name, patient_name
FROM images
WHERE thumb_filename IS NULL AND enabled = 1
`);
@@ -34,7 +34,7 @@ async function run() {
try {
// 1. Obter URL de download do Wasabi
const url = await storage.getDownloadPresignedUrl(row.filename, row.client_name, row.patient_name);
const url = await storage.getDownloadPresignedUrl(row.filename, row.clinic_name, row.client_name, row.patient_name);
if (!url) {
console.error(`❌ Não foi possível gerar URL de download para ${row.filename}`);
failCount++;
@@ -57,7 +57,7 @@ async function run() {
// 4. Salvar miniatura no Wasabi (usando a mesma lógica de rotas/VPS)
const thumbFilename = `thumb_${row.filename}`;
await storage.saveImage(thumbFilename, thumbBuffer, row.client_name, row.patient_name, false);
await storage.saveImage(thumbFilename, thumbBuffer, row.clinic_name, row.client_name, row.patient_name, false);
// 5. Atualizar o banco de dados
await db.run('UPDATE images SET thumb_filename = $1 WHERE id = $2', [thumbFilename, row.id]);
+1
View File
@@ -93,6 +93,7 @@ async function createTables() {
// Garantir que a coluna exista caso a tabela já tenha sido criada antes
await client.query('ALTER TABLE images ADD COLUMN IF NOT EXISTS thumb_filename TEXT;');
await client.query('ALTER TABLE images ADD COLUMN IF NOT EXISTS clinic_name VARCHAR(100);');
console.log('✅ Tabela images criada/verificada no PostgreSQL');
@@ -1 +1 @@
2.1.17
2.1.29
+97
View File
@@ -0,0 +1,97 @@
// ────────────────────────────────────────────────────────────────────────────
// migrate-to-wasabi.js — Migração one-off: envia arquivos locais (originais +
// thumbs) para o Wasabi e verifica via HEAD. NÃO apaga o local (segurança).
// Uso: docker exec rx_scoreodonto_app node /app/migrate-to-wasabi.js
// ────────────────────────────────────────────────────────────────────────────
const db = require('./database');
const storage = require('./storage');
const fs = require('fs');
const path = require('path');
const { S3Client, HeadObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/app/uploads';
const PROCESSED_DIR = process.env.PROCESSED_DIR || '/app/processed';
function sanitize(seg) {
if (!seg) return 'desconhecido';
return seg.trim().replace(/[\/\\?#%*:"<>|]/g, '_');
}
function contentTypeFor(filename) {
const ext = path.extname(filename).toLowerCase();
if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
if (ext === '.webp') return 'image/webp';
return 'image/png';
}
function findLocal(filename) {
for (const dir of [UPLOAD_DIR, PROCESSED_DIR]) {
const p = path.join(dir, filename);
if (fs.existsSync(p)) return p;
}
return null;
}
(async () => {
await db.initDatabase();
await storage.loadConfigFromDb();
const cfg = JSON.parse((await db.get("SELECT value FROM system_config WHERE key='wasabi_config'")).value);
const ep = cfg.wasabiEndpoint.startsWith('http') ? cfg.wasabiEndpoint : 'https://' + cfg.wasabiEndpoint;
const s3 = new S3Client({
region: cfg.wasabiRegion, endpoint: ep,
credentials: { accessKeyId: cfg.wasabiAccessKey, secretAccessKey: cfg.wasabiSecretKey },
forcePathStyle: true,
});
const bucket = cfg.wasabiBucket;
const rows = await db.all(
'SELECT id, filename, thumb_filename, clinic_name, client_name, patient_name FROM images ORDER BY id'
);
console.log(`\n🔎 ${rows.length} imagens no banco. Migrando para bucket "${bucket}"...\n`);
let uploaded = 0, already = 0, missingLocal = 0, failed = 0;
async function ensure(filename, clinic, client, patient) {
if (!filename) return;
const key = `${sanitize(clinic)}/${sanitize(client)}/${sanitize(patient)}/${filename}`;
// Já existe no Wasabi?
try { await s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key })); already++; return; }
catch (e) { if (e.name !== 'NotFound' && e.$metadata?.httpStatusCode !== 404) { /* outro erro: tenta upload mesmo assim */ } }
// Ler local
const local = findLocal(filename);
if (!local) { missingLocal++; return; }
try {
const body = await fs.promises.readFile(local);
await s3.send(new PutObjectCommand({
Bucket: bucket, Key: key,
Body: body, ContentType: contentTypeFor(filename),
}));
uploaded++;
if (uploaded % 50 === 0) console.log(`${uploaded} enviados (${already} já existiam)`);
} catch (e) { failed++; if (failed <= 10) console.log(` ❌ falha ${key}: ${e.name} ${e.message}`); }
}
// Monta lista de tarefas (original + thumb por imagem) e processa com concorrência
const tasks = [];
for (const r of rows) {
tasks.push([r.filename, r.clinic_name, r.client_name, r.patient_name]);
if (r.thumb_filename) tasks.push([r.thumb_filename, r.clinic_name, r.client_name, r.patient_name]);
}
const CONCURRENCY = Number(process.env.MIG_CONCURRENCY) || 40;
console.log(`📦 ${tasks.length} arquivos a verificar (concorrência ${CONCURRENCY})\n`);
let idx = 0;
async function worker() {
while (idx < tasks.length) {
const i = idx++;
const [f, cl, c, p] = tasks[i];
await ensure(f, cl, c, p);
}
}
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
console.log(`\n✅ Migração concluída:`);
console.log(` enviados agora: ${uploaded}`);
console.log(` já no Wasabi: ${already}`);
console.log(` sem local: ${missingLocal}`);
console.log(` falhas: ${failed}`);
process.exit(0);
})().catch(e => { console.error('ERRO FATAL:', e); process.exit(1); });
+3 -3
View File
@@ -15,7 +15,7 @@ async function run() {
// Buscar todas as imagens que possuem miniatura
const rows = await db.all(`
SELECT id, filename, thumb_filename, client_name, patient_name
SELECT id, filename, thumb_filename, clinic_name, client_name, patient_name
FROM images
WHERE thumb_filename IS NOT NULL AND enabled = 1
`);
@@ -30,7 +30,7 @@ async function run() {
try {
// 1. Obter URL de download do Wasabi (baixar a ORIGINAL, não a miniatura pesada)
const url = await storage.getDownloadPresignedUrl(row.filename, row.client_name, row.patient_name);
const url = await storage.getDownloadPresignedUrl(row.filename, row.clinic_name, row.client_name, row.patient_name);
if (!url) {
console.error(`❌ Não foi possível gerar URL para a imagem original ${row.filename}`);
failCount++;
@@ -52,7 +52,7 @@ async function run() {
const thumbBuffer = await imageProcessor.getThumbnail(imageBuffer, 400, 400);
// 4. Salvar miniatura substituindo a antiga
await storage.saveImage(row.thumb_filename, thumbBuffer, row.client_name, row.patient_name, false);
await storage.saveImage(row.thumb_filename, thumbBuffer, row.clinic_name, row.client_name, row.patient_name, false);
console.log(`✅ Miniatura ultra-leve gerada e salva com sucesso para ID ${row.id} (${thumbBuffer.length} bytes)`);
successCount++;
+20 -18
View File
@@ -34,19 +34,20 @@ router.post('/upload', async (req, res) => {
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, clientName, patientName, false);
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, clientName, patientName, false);
await storage.saveImage(thumbFilename, thumbBuffer, clinicName, clientName, patientName, false);
} catch (e) {
console.error('Erro ao gerar thumbnail manual:', e);
}
@@ -56,9 +57,9 @@ router.post('/upload', async (req, res) => {
const result = await db.run(
`INSERT INTO images (
image_guid, patient_name, client_name, original_filename, filename, thumb_filename, enabled, doctor, remark
) VALUES ($1, $2, $3, $4, $5, $6, 1, $7, $8)`,
[guid, patientName, clientName, 'manual_upload.png', filename, thumbFilename, doctor, remark]
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
@@ -217,7 +218,7 @@ router.delete('/by-patient', async (req, res) => {
const images = await db.all('SELECT * FROM images WHERE patient_name = $1', [patientName]);
for (const image of images) {
await storage.deleteImage(image.filename, image.client_name, image.patient_name);
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]);
@@ -280,9 +281,9 @@ router.get('/:id/transformations', async (req, res) => {
let originalBuffer;
try {
originalBuffer = await storage.getImageBuffer(originalImage.filename, originalImage.client_name, originalImage.patient_name, false);
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.client_name, originalImage.patient_name, true);
originalBuffer = await storage.getImageBuffer(originalImage.filename, originalImage.clinic_name, originalImage.client_name, originalImage.patient_name, true);
}
const transformations = [
@@ -336,9 +337,9 @@ router.post('/:id/transform', async (req, res) => {
// Processar a imagem
let originalBuffer;
try {
originalBuffer = await storage.getImageBuffer(realOriginal.filename, realOriginal.client_name, realOriginal.patient_name, false);
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.client_name, realOriginal.patient_name, true);
originalBuffer = await storage.getImageBuffer(realOriginal.filename, realOriginal.clinic_name, realOriginal.client_name, realOriginal.patient_name, true);
}
let processed = await imageProcessor.rotate(originalBuffer, rotation);
@@ -347,7 +348,7 @@ router.post('/:id/transform', async (req, res) => {
// 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.client_name, realOriginal.patient_name, true);
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]);
@@ -357,12 +358,13 @@ router.post('/:id/transform', async (req, res) => {
// Inserir a nova imagem como a versão ativa
const result = await db.run(
`INSERT INTO images (
image_guid, patient_name, client_name, original_filename, filename,
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)`,
) 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,
@@ -457,7 +459,7 @@ router.delete('/:id', async (req, res) => {
}
// Apagar arquivo local e do S3
await storage.deleteImage(image.filename, image.client_name, image.patient_name);
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]);
@@ -475,7 +477,7 @@ router.delete('/:id', async (req, res) => {
// ================================================================
router.get('/:id/file-url', async (req, res) => {
try {
const image = await db.get('SELECT filename, client_name, patient_name FROM images WHERE id = $1', [req.params.id]);
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;
@@ -484,7 +486,7 @@ router.get('/:id/file-url', async (req, res) => {
} else {
// Se o arquivo está no Wasabi, reconstroem a URL
try {
const wasabiUrl = await storage.getImageUrl(image.filename, image.client_name, image.patient_name);
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);
@@ -524,7 +526,7 @@ router.put('/:id/toggle-enabled', async (req, res) => {
router.post('/backfill-thumbs', async (req, res) => {
try {
const rows = await db.all(
`SELECT id, filename, client_name, patient_name
`SELECT id, filename, clinic_name, client_name, patient_name
FROM images
WHERE thumb_filename IS NULL AND enabled = true
ORDER BY created_at DESC`
@@ -551,7 +553,7 @@ async function runThumbBackfill(rows) {
if (!imgBuffer) { failed++; continue; }
const thumbBuffer = await imageProcessor.getThumbnail(imgBuffer);
const thumbFilename = `thumb_${row.filename.replace(/\.[^.]+$/, '')}.jpg`;
await storage.saveImage(thumbFilename, thumbBuffer, 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]);
done++;
} catch (e) {
+22 -14
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
@@ -938,6 +940,7 @@ io.on('connection', (socket) => {
`INSERT INTO images (
image_guid,
patient_name,
clinic_name,
client_name,
original_filename,
filename,
@@ -946,10 +949,11 @@ io.on('connection', (socket) => {
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,
@@ -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,7 +1056,8 @@ 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) => {
@@ -1066,6 +1072,7 @@ io.on('connection', (socket) => {
`INSERT INTO images (
image_guid,
patient_name,
clinic_name,
client_name,
original_filename,
filename,
@@ -1074,10 +1081,11 @@ io.on('connection', (socket) => {
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) {
+27 -15
View File
@@ -108,15 +108,16 @@ function sanitizePathSegment(segment) {
/**
* Retorna a URL assinada de uma imagem no Wasabi (ou null se não estiver lá ou Wasabi desativado)
*/
async function getImageUrl(filename, clientName, patientName, isProcessed = false, expirySeconds = 3600) {
async function getImageUrl(filename, clinicName, clientName, patientName, isProcessed = false, expirySeconds = 3600) {
if (!s3 || !currentBucket || !clientName || !patientName) {
return null;
}
try {
const cleanClinic = sanitizePathSegment(clinicName);
const cleanClient = sanitizePathSegment(clientName);
const cleanPatient = sanitizePathSegment(patientName);
const key = `${cleanClient}/${cleanPatient}/${filename}`;
const key = `${cleanClinic}/${cleanClient}/${cleanPatient}/${filename}`;
// Gerar URL assinada com expiração (padrão: 1 hora)
const url = await getSignedUrl(
@@ -135,7 +136,7 @@ async function getImageUrl(filename, clientName, patientName, isProcessed = fals
* Salva a imagem. Primeiro no disco local e depois, se o Wasabi estiver ativo, faz o upload.
* Remove a versão local temporária se o upload pro Wasabi der certo.
*/
async function saveImage(filename, buffer, clientName, patientName, isProcessed = false) {
async function saveImage(filename, buffer, clinicName, clientName, patientName, isProcessed = false) {
const targetDir = isProcessed ? PROCESSED_DIR : UPLOAD_DIR;
const destPath = path.join(targetDir, filename);
@@ -146,9 +147,10 @@ async function saveImage(filename, buffer, clientName, patientName, isProcessed
// 2. Se Wasabi estiver ativo, despachar para a nuvem
if (s3 && currentBucket) {
try {
const cleanClinic = sanitizePathSegment(clinicName);
const cleanClient = sanitizePathSegment(clientName);
const cleanPatient = sanitizePathSegment(patientName);
const key = `${cleanClient}/${cleanPatient}/${filename}`;
const key = `${cleanClinic}/${cleanClient}/${cleanPatient}/${filename}`;
const ext = path.extname(filename).toLowerCase();
let contentType = 'image/png';
@@ -185,7 +187,7 @@ async function saveImage(filename, buffer, clientName, patientName, isProcessed
/**
* Tenta ler o arquivo local. Se não existir, tenta baixar do Wasabi.
*/
async function getImageBuffer(filename, clientName = null, patientName = null, isProcessed = false) {
async function getImageBuffer(filename, clinicName = null, clientName = null, patientName = null, isProcessed = false) {
const targetDir = isProcessed ? PROCESSED_DIR : UPLOAD_DIR;
const localPath = path.join(targetDir, filename);
@@ -195,13 +197,15 @@ async function getImageBuffer(filename, clientName = null, patientName = null, i
if (s3 && currentBucket) {
try {
let clName = clinicName;
let cName = clientName;
let pName = patientName;
if (!cName || !pName) {
try {
const row = await db.get('SELECT client_name, patient_name FROM images WHERE filename = $1 OR thumb_filename = $2 LIMIT 1', [filename, filename]);
const row = await db.get('SELECT clinic_name, client_name, patient_name FROM images WHERE filename = $1 OR thumb_filename = $2 LIMIT 1', [filename, filename]);
if (row) {
clName = row.clinic_name;
cName = row.client_name;
pName = row.patient_name;
}
@@ -210,9 +214,10 @@ async function getImageBuffer(filename, clientName = null, patientName = null, i
}
}
const cleanClinic = sanitizePathSegment(clName);
const cleanClient = sanitizePathSegment(cName);
const cleanPatient = sanitizePathSegment(pName);
const key = `${cleanClient}/${cleanPatient}/${filename}`;
const key = `${cleanClinic}/${cleanClient}/${cleanPatient}/${filename}`;
const response = await s3.send(new GetObjectCommand({
Bucket: currentBucket,
@@ -247,7 +252,7 @@ async function getImageBuffer(filename, clientName = null, patientName = null, i
/**
* Exclui a imagem do disco local e do S3.
*/
async function deleteImage(filename, clientName = null, patientName = null) {
async function deleteImage(filename, clinicName = null, clientName = null, patientName = null) {
const localPaths = [
path.join(UPLOAD_DIR, filename),
path.join(PROCESSED_DIR, filename)
@@ -263,20 +268,23 @@ async function deleteImage(filename, clientName = null, patientName = null) {
if (s3 && currentBucket) {
try {
let clName = clinicName;
let cName = clientName;
let pName = patientName;
if (!cName || !pName) {
const row = await db.get('SELECT client_name, patient_name FROM images WHERE filename = $1 LIMIT 1', [filename]);
const row = await db.get('SELECT clinic_name, client_name, patient_name FROM images WHERE filename = $1 LIMIT 1', [filename]);
if (row) {
clName = row.clinic_name;
cName = row.client_name;
pName = row.patient_name;
}
}
const cleanClinic = sanitizePathSegment(clName);
const cleanClient = sanitizePathSegment(cName);
const cleanPatient = sanitizePathSegment(pName);
const key = `${cleanClient}/${cleanPatient}/${filename}`;
const key = `${cleanClinic}/${cleanClient}/${cleanPatient}/${filename}`;
await s3.send(new DeleteObjectCommand({
Bucket: currentBucket,
@@ -296,16 +304,18 @@ async function deleteImage(filename, clientName = null, patientName = null) {
* Gera uma URL pré-assinada para download da imagem diretamente do Wasabi.
* Retorna null se Wasabi não estiver configurado ou se não achar as chaves.
*/
async function getDownloadPresignedUrl(filename, clientName = null, patientName = null) {
async function getDownloadPresignedUrl(filename, clinicName = null, clientName = null, patientName = null) {
if (!s3 || !currentBucket) return null;
try {
let clName = clinicName;
let cName = clientName;
let pName = patientName;
if (!cName || !pName) {
const row = await db.get('SELECT client_name, patient_name FROM images WHERE filename = $1 OR thumb_filename = $2 LIMIT 1', [filename, filename]);
const row = await db.get('SELECT clinic_name, client_name, patient_name FROM images WHERE filename = $1 OR thumb_filename = $2 LIMIT 1', [filename, filename]);
if (row) {
clName = row.clinic_name;
cName = row.client_name;
pName = row.patient_name;
} else {
@@ -313,9 +323,10 @@ async function getDownloadPresignedUrl(filename, clientName = null, patientName
}
}
const cleanClinic = sanitizePathSegment(clName);
const cleanClient = sanitizePathSegment(cName);
const cleanPatient = sanitizePathSegment(pName);
const key = `${cleanClient}/${cleanPatient}/${filename}`;
const key = `${cleanClinic}/${cleanClient}/${cleanPatient}/${filename}`;
const command = new GetObjectCommand({
Bucket: currentBucket,
@@ -334,13 +345,14 @@ async function getDownloadPresignedUrl(filename, clientName = null, patientName
/**
* Gera uma URL pré-assinada para upload da imagem diretamente para o Wasabi.
*/
async function getUploadPresignedUrl(filename, clientName, patientName, contentType = 'image/png') {
async function getUploadPresignedUrl(filename, clinicName, clientName, patientName, contentType = 'image/png') {
if (!s3 || !currentBucket) return null;
try {
const cleanClinic = sanitizePathSegment(clinicName);
const cleanClient = sanitizePathSegment(clientName);
const cleanPatient = sanitizePathSegment(patientName);
const key = `${cleanClient}/${cleanPatient}/${filename}`;
const key = `${cleanClinic}/${cleanClient}/${cleanPatient}/${filename}`;
const command = new PutObjectCommand({
Bucket: currentBucket,