Files
rx.scoreodonto.com/dental-server/migrate-to-wasabi.js
T
VPS 4 Deploy Agent 21f33c88a4
continuous-integration/webhook Deploy concluído (VPS4)
feat(storage): reorganiza Wasabi com estrutura clinic_name/pc_name/patient_name
- 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>
2026-05-31 01:56:18 +02:00

98 lines
4.3 KiB
JavaScript

// ────────────────────────────────────────────────────────────────────────────
// 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); });