21f33c88a4
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>
98 lines
4.3 KiB
JavaScript
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); });
|