fix(sync): re-upload correto quando arquivo foi deletado do storage
continuous-integration/webhook Deploy concluído (VPS4)
continuous-integration/webhook Deploy concluído (VPS4)
- storage.js: adiciona fileExists() com HeadObject (sem baixar o arquivo) - check-image-exists: verifica existência física no storage, não só no banco; se registro existe mas arquivo não, apaga o registro órfão e retorna exists=false - send-image: mesma lógica — registro órfão é removido e re-upload prossegue - Remove migrate-to-wasabi.js do repositório Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,97 +0,0 @@
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// 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); });
|
||||
+35
-21
@@ -877,26 +877,30 @@ io.on('connection', (socket) => {
|
||||
throw new Error('Metadata incompleta. fileName é obrigatório.');
|
||||
}
|
||||
|
||||
// Verificar se a imagem já foi inserida no banco
|
||||
// Verificar se a imagem já existe no banco E no storage
|
||||
const existing = await db.get(
|
||||
"SELECT id, filename FROM images WHERE original_filename = $1 AND enabled = 1 LIMIT 1",
|
||||
"SELECT id, filename, clinic_name, client_name, patient_name FROM images WHERE original_filename = $1 AND enabled = 1 LIMIT 1",
|
||||
[data.metadata.fileName]
|
||||
);
|
||||
if (existing) {
|
||||
console.log(`⚠️ Imagem ${data.metadata.fileName} já existe no servidor (ID: ${existing.id}). Ignorando upload.`);
|
||||
const ackData = {
|
||||
success: true,
|
||||
imageId: existing.id,
|
||||
message: 'Imagem já existe no servidor',
|
||||
filename: existing.filename,
|
||||
originalFilename: data.metadata.fileName,
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
socket.emit('image-received', ackData);
|
||||
if (typeof callback === 'function') {
|
||||
callback(ackData);
|
||||
const physicallyExists = await storage.fileExists(existing.filename, existing.clinic_name, existing.client_name, existing.patient_name);
|
||||
if (physicallyExists) {
|
||||
console.log(`⚠️ Imagem ${data.metadata.fileName} já existe no servidor (ID: ${existing.id}). Ignorando upload.`);
|
||||
const ackData = {
|
||||
success: true,
|
||||
imageId: existing.id,
|
||||
message: 'Imagem já existe no servidor',
|
||||
filename: existing.filename,
|
||||
originalFilename: data.metadata.fileName,
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
socket.emit('image-received', ackData);
|
||||
if (typeof callback === 'function') callback(ackData);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
// Registro órfão (arquivo deletado do storage) — remove para re-upload limpo
|
||||
console.log(`🔄 Registro órfão encontrado para ${data.metadata.fileName} (ID: ${existing.id}). Removendo para re-upload.`);
|
||||
await db.run('DELETE FROM images WHERE id = $1', [existing.id]);
|
||||
}
|
||||
|
||||
// Decodificar imagem base64
|
||||
@@ -1125,18 +1129,28 @@ io.on('connection', (socket) => {
|
||||
if (typeof callback === 'function') callback({ success: false, error: 'fileName é obrigatório' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const existing = await db.get(
|
||||
"SELECT id, filename FROM images WHERE original_filename = $1 AND enabled = 1 LIMIT 1",
|
||||
"SELECT id, filename, clinic_name, client_name, patient_name FROM images WHERE original_filename = $1 AND enabled = 1 LIMIT 1",
|
||||
[data.fileName]
|
||||
);
|
||||
|
||||
|
||||
// Registro no banco só conta se o arquivo realmente existir no storage
|
||||
let physicallyExists = false;
|
||||
if (existing) {
|
||||
physicallyExists = await storage.fileExists(existing.filename, existing.clinic_name, existing.client_name, existing.patient_name);
|
||||
if (!physicallyExists) {
|
||||
// Apaga o registro órfão para o re-upload criar um novo
|
||||
await db.run('DELETE FROM images WHERE id = $1', [existing.id]);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback({
|
||||
success: true,
|
||||
exists: !!existing,
|
||||
imageId: existing ? existing.id : null,
|
||||
filename: existing ? existing.filename : null
|
||||
exists: physicallyExists,
|
||||
imageId: physicallyExists ? existing.id : null,
|
||||
filename: physicallyExists ? existing.filename : null
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { S3Client, GetObjectCommand, DeleteObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const { S3Client, GetObjectCommand, DeleteObjectCommand, PutObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
||||
const { Upload } = require('@aws-sdk/lib-storage');
|
||||
const fs = require('fs').promises;
|
||||
@@ -369,12 +369,54 @@ async function getUploadPresignedUrl(filename, clinicName, clientName, patientNa
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se o arquivo existe no storage (local ou Wasabi) sem baixar o conteúdo.
|
||||
* Retorna true se encontrado em qualquer um dos dois lugares.
|
||||
*/
|
||||
async function fileExists(filename, clinicName = null, clientName = null, patientName = null) {
|
||||
// Verificar local primeiro
|
||||
const localPaths = [
|
||||
path.join(UPLOAD_DIR, filename),
|
||||
path.join(PROCESSED_DIR, filename),
|
||||
];
|
||||
for (const p of localPaths) {
|
||||
if (fsSync.existsSync(p)) return true;
|
||||
}
|
||||
|
||||
if (!s3 || !currentBucket) return false;
|
||||
|
||||
try {
|
||||
let clName = clinicName;
|
||||
let cName = clientName;
|
||||
let pName = patientName;
|
||||
|
||||
if (!cName || !pName) {
|
||||
try {
|
||||
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; }
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const key = `${sanitizePathSegment(clName)}/${sanitizePathSegment(cName)}/${sanitizePathSegment(pName)}/${filename}`;
|
||||
await s3.send(new HeadObjectCommand({ Bucket: currentBucket, Key: key }));
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e.name === 'NotFound' || e.$metadata?.httpStatusCode === 404) return false;
|
||||
// Erro de rede/auth — assume não existe para forçar re-upload
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadConfigFromDb,
|
||||
saveImage,
|
||||
getImageBuffer,
|
||||
getImageUrl,
|
||||
deleteImage,
|
||||
fileExists,
|
||||
getDownloadPresignedUrl,
|
||||
getUploadPresignedUrl,
|
||||
isWasabiEnabled: () => !!(s3 && currentBucket),
|
||||
|
||||
Reference in New Issue
Block a user