feat(migration): suporte a upload de arquivos RAR, SQL e SQL.GZ
- Dockerfile: instala p7zip no Alpine para extração de RAR via 7z - migration/routes.ts: detecta .rar, .sql.gz e .sql; extrai RAR com 7z, descomprime .gz com gunzip, analisa tabelas do dump SQL - Migration.tsx: accept e hint atualizados para incluir RAR/SQL Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
66d8dfbfcc
commit
9bfc888d28
|
|
@ -10,6 +10,8 @@ RUN npm run build
|
||||||
|
|
||||||
FROM node:20-alpine AS runner
|
FROM node:20-alpine AS runner
|
||||||
|
|
||||||
|
RUN apk add --no-cache p7zip
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,7 @@ export default function Migration() {
|
||||||
<div className="mt-2 border-2 border-dashed rounded-lg p-6 text-center">
|
<div className="mt-2 border-2 border-dashed rounded-lg p-6 text-center">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".zip,.json,.csv"
|
accept=".zip,.json,.csv,.rar,.sql,.sql.gz"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
id="backup-file"
|
id="backup-file"
|
||||||
onChange={e => setUploadFile(e.target.files?.[0] || null)}
|
onChange={e => setUploadFile(e.target.files?.[0] || null)}
|
||||||
|
|
@ -286,7 +286,7 @@ export default function Migration() {
|
||||||
<p className="mt-2 text-sm text-gray-500">
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
{uploadFile ? uploadFile.name : "Clique para selecionar"}
|
{uploadFile ? uploadFile.name : "Clique para selecionar"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-400 mt-1">ZIP (MongoDB), JSON ou CSV</p>
|
<p className="text-xs text-gray-400 mt-1">ZIP (MongoDB), RAR, SQL, SQL.GZ, JSON ou CSV</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,8 @@ router.post('/upload', upload.single('file'), async (req, res) => {
|
||||||
if (fileName.endsWith('.zip')) sourceType = 'mongodb';
|
if (fileName.endsWith('.zip')) sourceType = 'mongodb';
|
||||||
else if (fileName.endsWith('.json')) sourceType = 'json';
|
else if (fileName.endsWith('.json')) sourceType = 'json';
|
||||||
else if (fileName.endsWith('.csv')) sourceType = 'csv';
|
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({
|
const [job] = await db.insert(migrationJobs).values({
|
||||||
name: name || `Migração ${new Date().toLocaleDateString('pt-BR')}`,
|
name: name || `Migração ${new Date().toLocaleDateString('pt-BR')}`,
|
||||||
|
|
@ -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));
|
const [updatedJob] = await db.select().from(migrationJobs).where(eq(migrationJobs.id, job.id));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue