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
|
||||
|
||||
RUN apk add --no-cache p7zip
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
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">
|
||||
<input
|
||||
type="file"
|
||||
accept=".zip,.json,.csv"
|
||||
accept=".zip,.json,.csv,.rar,.sql,.sql.gz"
|
||||
className="hidden"
|
||||
id="backup-file"
|
||||
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">
|
||||
{uploadFile ? uploadFile.name : "Clique para selecionar"}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Reference in New Issue