diff --git a/Dockerfile b/Dockerfile index 7ad3611..0316036 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,8 @@ RUN npm run build FROM node:20-alpine AS runner +RUN apk add --no-cache p7zip + WORKDIR /app COPY --from=builder /app/dist ./dist diff --git a/client/src/pages/Migration.tsx b/client/src/pages/Migration.tsx index c4293d3..18e7884 100644 --- a/client/src/pages/Migration.tsx +++ b/client/src/pages/Migration.tsx @@ -275,7 +275,7 @@ export default function Migration() {
setUploadFile(e.target.files?.[0] || null)} @@ -286,7 +286,7 @@ export default function Migration() {

{uploadFile ? uploadFile.name : "Clique para selecionar"}

-

ZIP (MongoDB), JSON ou CSV

+

ZIP (MongoDB), RAR, SQL, SQL.GZ, JSON ou CSV

diff --git a/server/migration/routes.ts b/server/migration/routes.ts index a55dfdf..ca374c9 100644 --- a/server/migration/routes.ts +++ b/server/migration/routes.ts @@ -90,6 +90,8 @@ router.post('/upload', upload.single('file'), async (req, res) => { if (fileName.endsWith('.zip')) sourceType = 'mongodb'; else if (fileName.endsWith('.json')) sourceType = 'json'; else if (fileName.endsWith('.csv')) sourceType = 'csv'; + else if (fileName.endsWith('.rar')) sourceType = 'sql-rar'; + else if (fileName.endsWith('.sql.gz') || fileName.endsWith('.sql')) sourceType = 'sql'; const [job] = await db.insert(migrationJobs).values({ name: name || `Migração ${new Date().toLocaleDateString('pt-BR')}`, @@ -108,26 +110,26 @@ router.post('/upload', upload.single('file'), async (req, res) => { await execAsync(`unzip -o "${filePath}" -d "${extractDir}"`); - const subdirs = fs.readdirSync(extractDir).filter(f => - fs.statSync(path.join(extractDir, f)).isDirectory() && + const subdirs = fs.readdirSync(extractDir).filter(f => + fs.statSync(path.join(extractDir, f)).isDirectory() && fs.readdirSync(path.join(extractDir, f)).some(sf => sf.endsWith('.bson')) ); - const bsonDir = subdirs.length > 0 + const bsonDir = subdirs.length > 0 ? path.join(extractDir, subdirs[0]) : extractDir; await db.update(migrationJobs) - .set({ + .set({ status: 'analyzing', importConfig: { extractPath: bsonDir } }) .where(eq(migrationJobs.id, job.id)); const analysis = analyzeBackupDirectory(bsonDir); - + await db.update(migrationJobs) - .set({ + .set({ status: 'mapping', totalRecords: analysis.totalRecords, analysisResult: analysis @@ -148,6 +150,87 @@ router.post('/upload', upload.single('file'), async (req, res) => { }); } } + } else if (sourceType === 'sql-rar') { + const extractDir = path.join(UPLOAD_DIR, `job-${job.id}`); + fs.mkdirSync(extractDir, { recursive: true }); + + await db.update(migrationJobs) + .set({ status: 'analyzing' }) + .where(eq(migrationJobs.id, job.id)); + + await execAsync(`7z x "${filePath}" -o"${extractDir}" -y`); + + const allFiles = fs.readdirSync(extractDir); + const sqlGzFile = allFiles.find(f => f.endsWith('.sql.gz')); + const sqlFile = allFiles.find(f => f.endsWith('.sql')); + + let finalSqlPath: string; + if (sqlGzFile) { + const gzPath = path.join(extractDir, sqlGzFile); + finalSqlPath = gzPath.replace(/\.gz$/, ''); + await execAsync(`gunzip -f "${gzPath}"`); + } else if (sqlFile) { + finalSqlPath = path.join(extractDir, sqlFile); + } else { + throw new Error('Nenhum arquivo .sql ou .sql.gz encontrado dentro do RAR'); + } + + const sqlContent = fs.readFileSync(finalSqlPath, 'utf8'); + const tableMatches = sqlContent.match(/CREATE TABLE\s+`?(\w+)`?/gi) || []; + const tables = tableMatches.map(m => m.replace(/CREATE TABLE\s+`?/i, '').replace(/`/g, '').trim()); + const lineCount = sqlContent.split('\n').length; + + await db.update(migrationJobs) + .set({ + status: 'mapping', + totalRecords: lineCount, + analysisResult: { + type: 'sql', + sqlPath: finalSqlPath, + tables, + tableCount: tables.length, + lineCount, + fileSizeBytes: fs.statSync(finalSqlPath).size + }, + importConfig: { sqlPath: finalSqlPath } + }) + .where(eq(migrationJobs.id, job.id)); + } else if (sourceType === 'sql') { + const extractDir = path.join(UPLOAD_DIR, `job-${job.id}`); + fs.mkdirSync(extractDir, { recursive: true }); + + await db.update(migrationJobs) + .set({ status: 'analyzing' }) + .where(eq(migrationJobs.id, job.id)); + + let finalSqlPath: string; + if (fileName.endsWith('.sql.gz')) { + finalSqlPath = path.join(extractDir, fileName.replace(/\.gz$/, '')); + await execAsync(`gunzip -c "${filePath}" > "${finalSqlPath}"`); + } else { + finalSqlPath = filePath; + } + + const sqlContent = fs.readFileSync(finalSqlPath, 'utf8'); + const tableMatches = sqlContent.match(/CREATE TABLE\s+`?(\w+)`?/gi) || []; + const tables = tableMatches.map(m => m.replace(/CREATE TABLE\s+`?/i, '').replace(/`/g, '').trim()); + const lineCount = sqlContent.split('\n').length; + + await db.update(migrationJobs) + .set({ + status: 'mapping', + totalRecords: lineCount, + analysisResult: { + type: 'sql', + sqlPath: finalSqlPath, + tables, + tableCount: tables.length, + lineCount, + fileSizeBytes: fs.statSync(finalSqlPath).size + }, + importConfig: { sqlPath: finalSqlPath } + }) + .where(eq(migrationJobs.id, job.id)); } const [updatedJob] = await db.select().from(migrationJobs).where(eq(migrationJobs.id, job.id));