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
+29 -27
View File
@@ -34,31 +34,32 @@ 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);
}
const doctor = patientData.doctor || '';
const remark = patientData.remark || '';
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,9 +218,9 @@ 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]);
res.json({ success: true, count: images.length });
} catch (error) {
@@ -280,11 +281,11 @@ 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 = [
{ name: 'Original', rotation: 0, flipH: false, flipV: false },
{ name: 'Flip Horizontal', rotation: 0, flipH: true, flipV: false },
@@ -336,18 +337,18 @@ 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);
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.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,8 +459,8 @@ 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) {