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